Skip to main content

commonware_storage/qmdb/sync/
compact.rs

1//! Compact sync for compact-storage qmdbs.
2//!
3//! Compact sync does not transfer or reconstruct the full historical operation log. Instead, the
4//! source serves the minimum authenticated state needed to recreate the latest committed compact db
5//! state:
6//!
7//! - the total committed leaf count,
8//! - the compact frontier's pinned nodes for that leaf count,
9//! - the final commit operation, and
10//! - a proof authenticating that final commit against the requested root.
11//!
12//! # What compact dbs store
13//!
14//! A compact db's only persistent state is its witness journal (`qmdb::compact::witness`), whose
15//! entries each snapshot one committed state (commit operation, proof, and frontier pins).
16//! The in-memory compact Merkle ([`crate::merkle::compact`]) is rebuilt from the journal tip on
17//! reopen. Without the witness, a compact db could recover its root and continue appending, but
18//! it could not serve compact sync to another node.
19//!
20//! # When compact state changes
21//!
22//! The servable compact state advances only on durable persistence:
23//!
24//! - [`sync`] verifies the final commit proof and compact frontier before database construction.
25//! - [`Database::from_validated_state`] reconstructs the already-validated state without
26//!   persisting it.
27//! - Compact db-local commits append one witness entry during `sync`.
28//! - `rewind` restores the frontier and the witness from the target journal entry.
29//!
30//! Unsynced in-memory mutations are therefore intentionally not servable: `target()` and
31//! compact-state responses lag behind `apply_batch()` until the db's next sync.
32//!
33//! # Safety and invariants
34//!
35//! The compact path relies on these invariants:
36//!
37//! - the served commit proof must authenticate the final commit at `leaf_count - 1`,
38//! - reopen and rewind must re-verify the persisted witness against the root recomputed from the
39//!   frontier rebuilt from the same journal entry, and
40//! - reconstructed state must not be persisted until the db recomputes the requested root locally.
41//!
42//! If those invariants are violated by missing or corrupted persisted data, compact db reopen fails
43//! with `DataCorrupted` rather than silently serving or restoring mismatched state.
44
45use crate::{
46    merkle::{Family, Location, Proof},
47    qmdb::{
48        self,
49        any::{value::ValueEncoding, FixedValue, VariableValue},
50        immutable::{
51            fixed::{Db as ImmutableFixedDb, Operation as ImmutableFixedOp},
52            variable::{Db as ImmutableVariableDb, Operation as ImmutableVariableOp},
53            CompactDb as ImmutableCompactDb, Operation as ImmutableOp,
54        },
55        keyless::{
56            fixed::{Db as KeylessFixedDb, Operation as KeylessFixedOp},
57            variable::{Db as KeylessVariableDb, Operation as KeylessVariableOp},
58            CompactDb as KeylessCompactDb, Operation as KeylessOp,
59        },
60        operation::Key,
61        sync::{EngineError, Error},
62        verify_proof,
63    },
64    translator::Translator,
65};
66use commonware_codec::{
67    Encode, EncodeSize, Error as CodecError, RangeCfg, Read, ReadExt as _, Write,
68};
69use commonware_cryptography::{Digest, Hasher};
70use commonware_macros::{boxed, select};
71use commonware_parallel::Strategy;
72use commonware_runtime::{reschedule, Buf, BufMut, Clock, Metrics, Storage, Supervisor};
73use commonware_utils::{
74    channel::{mpsc, oneshot},
75    sync::{AsyncRwLock, TracedAsyncRwLock},
76    Array,
77};
78use futures::future::{pending, Either};
79use std::{future::Future, num::NonZeroU64, sync::Arc};
80
81/// Compact-sync target for a compact-storage database.
82///
83/// Compact sync authenticates only the final committed root and total leaf count. Unlike replay
84/// sync, there is no lower replay bound here because compact sync does not transfer or reconstruct
85/// historical operations.
86#[derive(Debug)]
87pub struct Target<F: Family, D: Digest> {
88    /// Authenticated root of the committed compact state.
89    pub root: D,
90    /// Total committed operations/leaves in that state.
91    pub leaf_count: Location<F>,
92}
93
94impl<F: Family, D: Digest> Target<F, D> {
95    const INVALID_LEAF_COUNT: &'static str = "leaf_count must be in 1..=MAX_LEAVES";
96
97    /// Create a compact-sync target.
98    pub const fn new(root: D, leaf_count: Location<F>) -> Self {
99        Self { root, leaf_count }
100    }
101
102    /// Validate a compact target that may have been constructed programmatically.
103    pub fn validate(&self) -> Result<(), &'static str> {
104        if !self.leaf_count.is_valid() || self.leaf_count == 0 {
105            return Err(Self::INVALID_LEAF_COUNT);
106        }
107        Ok(())
108    }
109}
110
111impl<F: Family, D: Digest> Clone for Target<F, D> {
112    fn clone(&self) -> Self {
113        Self {
114            root: self.root,
115            leaf_count: self.leaf_count,
116        }
117    }
118}
119
120impl<F: Family, D: Digest> PartialEq for Target<F, D> {
121    fn eq(&self, other: &Self) -> bool {
122        self.root == other.root && self.leaf_count == other.leaf_count
123    }
124}
125
126impl<F: Family, D: Digest> Eq for Target<F, D> {}
127
128impl<F: Family, D: Digest> Write for Target<F, D> {
129    fn write(&self, buf: &mut impl BufMut) {
130        self.root.write(buf);
131        self.leaf_count.write(buf);
132    }
133}
134
135impl<F: Family, D: Digest> EncodeSize for Target<F, D> {
136    fn encode_size(&self) -> usize {
137        self.root.encode_size() + self.leaf_count.encode_size()
138    }
139}
140
141impl<F: Family, D: Digest> Read for Target<F, D> {
142    type Cfg = ();
143
144    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
145        let root = D::read(buf)?;
146        let leaf_count = Location::<F>::read(buf)?;
147        let target = Self { root, leaf_count };
148        target.validate().map_err(|reason| {
149            CodecError::Invalid("storage::qmdb::sync::compact::Target", reason)
150        })?;
151        Ok(target)
152    }
153}
154
155#[cfg(feature = "arbitrary")]
156impl<F: Family, D: Digest> arbitrary::Arbitrary<'_> for Target<F, D>
157where
158    D: for<'a> arbitrary::Arbitrary<'a>,
159{
160    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
161        let root = u.arbitrary()?;
162        let leaf_count = Location::new(u.int_in_range(1..=*F::MAX_LEAVES)?);
163        Ok(Self { root, leaf_count })
164    }
165}
166
167/// Authenticated state for initializing a compact-storage database at a target root.
168#[derive(Clone, Debug)]
169pub struct State<F: Family, Op, D: Digest> {
170    /// Total number of operations/leaves in the target database.
171    pub leaf_count: Location<F>,
172    /// Pinned Merkle nodes for the current frontier.
173    pub pinned_nodes: Vec<D>,
174    /// The final commit operation at `leaf_count - 1`.
175    pub last_commit_op: Op,
176    /// Proof authenticating `last_commit_op` against the target root.
177    pub last_commit_proof: Proof<F, D>,
178}
179
180/// Compact state that has been validated against a target root.
181#[derive(Clone, Debug)]
182pub struct ValidatedState<F: Family, Op, D: Digest> {
183    /// The compact state fetched from a peer after validation.
184    pub state: State<F, Op, D>,
185    /// The target root that `state` was validated against.
186    pub root: D,
187}
188
189impl<F: Family, Op, D: Digest> Write for State<F, Op, D>
190where
191    Op: Write,
192{
193    fn write(&self, buf: &mut impl BufMut) {
194        self.leaf_count.write(buf);
195        self.pinned_nodes.write(buf);
196        self.last_commit_op.write(buf);
197        self.last_commit_proof.write(buf);
198    }
199}
200
201/// Result from a compact-state fetch.
202pub struct FetchResult<F: Family, Op, D: Digest> {
203    /// The fetched compact state.
204    pub state: State<F, Op, D>,
205    /// Callback used to report whether downstream validated the state.
206    pub callback: Option<oneshot::Sender<bool>>,
207}
208
209impl<F: Family, Op: std::fmt::Debug, D: Digest> std::fmt::Debug for FetchResult<F, Op, D> {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("FetchResult")
212            .field("state", &self.state)
213            .field("callback", &self.callback.as_ref().map(|_| "<callback>"))
214            .finish()
215    }
216}
217
218impl<F: Family, Op, D: Digest> From<State<F, Op, D>> for FetchResult<F, Op, D> {
219    fn from(state: State<F, Op, D>) -> Self {
220        Self {
221            state,
222            callback: None,
223        }
224    }
225}
226
227impl<F: Family, Op, D: Digest> EncodeSize for State<F, Op, D>
228where
229    Op: EncodeSize,
230{
231    fn encode_size(&self) -> usize {
232        self.leaf_count.encode_size()
233            + self.pinned_nodes.encode_size()
234            + self.last_commit_op.encode_size()
235            + self.last_commit_proof.encode_size()
236    }
237}
238
239impl<F: Family, Op, D: Digest> Read for State<F, Op, D>
240where
241    Op: Read,
242{
243    type Cfg = (RangeCfg<usize>, Op::Cfg, usize);
244
245    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
246        let (pinned_nodes_cfg, op_cfg, max_proof_digests) = cfg;
247        Ok(Self {
248            leaf_count: Location::<F>::read(buf)?,
249            pinned_nodes: Vec::<D>::read_cfg(buf, &(*pinned_nodes_cfg, ()))?,
250            last_commit_op: Op::read_cfg(buf, op_cfg)?,
251            last_commit_proof: Proof::<F, D>::read_cfg(buf, max_proof_digests)?,
252        })
253    }
254}
255
256/// Resolver-side errors for compact state serving.
257#[derive(Debug, thiserror::Error)]
258pub enum ServeError<F: Family, D: Digest> {
259    /// The source database returned an error while building compact state.
260    #[error("compact source database error: {0}")]
261    Database(#[from] qmdb::Error<F>),
262    /// The caller requested a target that compact sync cannot serve.
263    #[error("invalid compact target: {0}")]
264    InvalidTarget(&'static str),
265    /// The resolver wrapper did not currently hold a database.
266    #[error("compact source missing")]
267    MissingSource,
268    /// The caller requested a target different from the source's current witness.
269    #[error("stale compact target - requested {requested:?}, current {current:?}")]
270    StaleTarget {
271        requested: Target<F, D>,
272        current: Target<F, D>,
273    },
274}
275
276/// Trait for compact sync fetches from a source database.
277#[allow(clippy::type_complexity)]
278pub trait Resolver: Send + Sync + Clone + 'static {
279    /// The merkle family backing the resolver's proofs.
280    type Family: Family;
281
282    /// The digest type used in proofs returned by the resolver.
283    type Digest: Digest;
284
285    /// The type of operations returned by the resolver.
286    type Op;
287
288    /// The error type returned by the resolver.
289    type Error: std::error::Error + Send + 'static;
290
291    /// Fetch the authenticated state for `target`.
292    fn get_compact_state<'a>(
293        &'a self,
294        target: Target<Self::Family, Self::Digest>,
295    ) -> impl Future<Output = Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error>>
296           + Send
297           + 'a;
298}
299
300/// Marker trait for resolvers whose associated types match a specific compact-sync database.
301///
302/// This is a trait-alias pattern used to avoid repeating
303/// `Resolver<Family = DB::Family, Op = DB::Op, Digest = DB::Digest>`.
304/// Blanket-implemented for any matching [`Resolver`], so callers never implement this directly.
305pub trait CompactDbResolver<DB: Database>:
306    Resolver<Family = DB::Family, Op = DB::Op, Digest = DB::Digest>
307{
308}
309
310impl<DB, R> CompactDbResolver<DB> for R
311where
312    DB: Database,
313    R: Resolver<Family = DB::Family, Op = DB::Op, Digest = DB::Digest>,
314{
315}
316
317/// Database types that can be initialized directly from compact state.
318pub trait Database: Sized + Send {
319    type Family: Family;
320    type Op: Encode + Send;
321    type Config: Clone;
322    type Digest: Digest;
323    type Context: Storage + Clock + Metrics;
324    type Hasher: Hasher<Digest = Self::Digest>;
325
326    /// Build a database from authenticated state in memory.
327    ///
328    /// The caller has already validated `last_commit_proof` and the compact frontier against the
329    /// requested target root and passes the derived validation artifacts with the state. This
330    /// constructor must not durably persist anything; persistence happens only after the caller
331    /// re-checks that `Self::root()` matches the target root.
332    fn from_validated_state(
333        context: Self::Context,
334        config: Self::Config,
335        state: ValidatedState<Self::Family, Self::Op, Self::Digest>,
336    ) -> impl Future<Output = Result<Self, qmdb::Error<Self::Family>>> + Send;
337
338    /// Return the inactivity floor if the operation is a commit.
339    fn inactivity_floor(op: &Self::Op) -> Option<Location<Self::Family>>;
340
341    /// Get the root digest for final verification.
342    fn root(&self) -> Self::Digest;
343
344    /// Persist the compact-initialized state once the caller has verified its root.
345    fn persist_compact_state(
346        &mut self,
347    ) -> impl Future<Output = Result<(), qmdb::Error<Self::Family>>> + Send;
348}
349
350/// Configuration for compact synchronization into a compact-storage database.
351pub struct Config<DB, R>
352where
353    DB: Database,
354    R: CompactDbResolver<DB>,
355{
356    /// Runtime context for creating database components.
357    pub context: DB::Context,
358    /// Source resolver for fetching compact authenticated state.
359    pub resolver: R,
360    /// Sync target (root digest and total leaf count).
361    pub target: Target<DB::Family, DB::Digest>,
362    /// Database-specific configuration.
363    pub db_config: DB::Config,
364    /// Channel for receiving sync target updates. Each update supersedes the
365    /// current target, cancelling any in-flight attempt against it.
366    pub update_rx: Option<mpsc::Receiver<Target<DB::Family, DB::Digest>>>,
367    /// Channel that requests sync completion once the current target is reached.
368    ///
369    /// When `None`, sync completes as soon as the target is reached.
370    pub finish_rx: Option<mpsc::Receiver<()>>,
371    /// Channel used to notify an observer once the current target is reached.
372    /// If the receiver is dropped, sync completes with the current database.
373    pub reached_target_tx: Option<mpsc::Sender<Target<DB::Family, DB::Digest>>>,
374}
375
376/// Maximum queued target updates drained per scheduling tick.
377const MAX_UPDATE_DRAIN_PER_TICK: usize = 32;
378
379/// Drain all queued target updates without blocking, returning the newest.
380async fn drain_latest_target<T>(update_rx: &mut mpsc::Receiver<T>) -> Option<T> {
381    let mut latest = None;
382    let mut drained = 0usize;
383    loop {
384        match update_rx.try_recv() {
385            Ok(update) => {
386                latest = Some(update);
387                drained += 1;
388                if drained.is_multiple_of(MAX_UPDATE_DRAIN_PER_TICK) {
389                    reschedule().await;
390                }
391            }
392            Err(mpsc::error::TryRecvError::Empty | mpsc::error::TryRecvError::Disconnected) => {
393                return latest;
394            }
395        }
396    }
397}
398
399/// Create/open a compact-storage database and initialize it from compact authenticated state.
400///
401/// Unlike streaming sync, compact sync jumps directly to `target.leaf_count`. This path
402/// authenticates the final commit and frontier state for the target root rather than replaying a
403/// retained operation range.
404///
405/// Targets received on `update_rx` supersede the current target. When `finish_rx` is `Some(...)`,
406/// reaching a target parks sync until a finish signal or another target update arrives. Each
407/// reached target is reported on `reached_target_tx`.
408#[boxed]
409pub async fn sync<DB, R>(
410    config: Config<DB, R>,
411) -> Result<DB, Error<DB::Family, R::Error, DB::Digest>>
412where
413    DB: Database,
414    R: CompactDbResolver<DB>,
415{
416    let Config {
417        context,
418        resolver,
419        mut target,
420        db_config,
421        mut update_rx,
422        mut finish_rx,
423        reached_target_tx,
424    } = config;
425    let metrics = super::Metrics::new(&context);
426    let mut attempt = 0u64;
427    loop {
428        // Prefer the newest queued target before starting an attempt.
429        if let Some(update_rx) = update_rx.as_mut() {
430            if let Some(update) = drain_latest_target(update_rx).await {
431                target = update;
432            }
433        }
434        target
435            .validate()
436            .map_err(|reason| Error::Engine(EngineError::InvalidCompactTarget(reason)))?;
437        metrics.record_target(*target.leaf_count);
438
439        attempt += 1;
440        let update_future = update_rx.as_mut().map_or_else(
441            || Either::Right(pending()),
442            |update_rx| Either::Left(update_rx.recv()),
443        );
444        let db = select! {
445            update = update_future => {
446                let Some(update) = update else {
447                    update_rx = None;
448                    continue;
449                };
450                target = update;
451                continue;
452            },
453            db = attempt_sync(&context, attempt, &resolver, &db_config, &target) => db?,
454        };
455        metrics.record_synced(*target.leaf_count);
456
457        // A target queued while the attempt ran supersedes the result.
458        if let Some(update_rx) = update_rx.as_mut() {
459            if let Some(update) = drain_latest_target(update_rx).await {
460                target = update;
461                continue;
462            }
463        }
464
465        if let Some(reached_target_tx) = reached_target_tx.as_ref() {
466            if reached_target_tx.send(target.clone()).await.is_err() {
467                return Ok(db);
468            }
469        }
470
471        let Some(finish_rx) = finish_rx.as_mut() else {
472            return Ok(db);
473        };
474        let Some(update_rx) = update_rx.as_mut() else {
475            return Ok(db);
476        };
477        select! {
478            _ = finish_rx.recv() => return Ok(db),
479            update = update_rx.recv() => {
480                let Some(update) = update else {
481                    return Ok(db);
482                };
483                target = update;
484            },
485        }
486    }
487}
488
489/// Run one compact sync attempt against `target`.
490///
491/// Verification order:
492/// 1. Fetch the proposed compact state for `target`.
493/// 2. Verify the final commit proof against `target.root`.
494/// 3. Rebuild the compact frontier in memory and compare its root against `target.root`.
495/// 4. Build the compact db from that already-validated state.
496/// 5. Assert the db root still matches and persist the state.
497///
498/// A failure before the final persist leaves on-disk state untouched.
499async fn attempt_sync<DB, R>(
500    context: &DB::Context,
501    attempt: u64,
502    resolver: &R,
503    db_config: &DB::Config,
504    target: &Target<DB::Family, DB::Digest>,
505) -> Result<DB, Error<DB::Family, R::Error, DB::Digest>>
506where
507    DB: Database,
508    R: CompactDbResolver<DB>,
509{
510    // Compact sync has no request scheduler, so this loop is its retry boundary for bad peer
511    // responses. Resolver errors and local construction failures remain terminal.
512    loop {
513        let FetchResult { state, callback } = resolver
514            .get_compact_state(target.clone())
515            .await
516            .map_err(Error::Resolver)?;
517
518        // Validation failures describe a bad compact response. Reject it if the resolver supplied
519        // feedback, then fetch another candidate.
520        let validated_state = match validate_compact_state::<DB>(target, state) {
521            Ok(state) => state,
522            Err(err) => {
523                if let Some(callback) = callback {
524                    let _ = callback.send(false);
525                }
526                tracing::debug!(error = ?err, "compact state failed validation, will retry");
527                continue;
528            }
529        };
530
531        // The peer response has already authenticated the final commit and frontier. From here,
532        // construction should only fail for local database/storage reasons; a root mismatch is a
533        // bug in this path.
534        let mut db = DB::from_validated_state(
535            context.child("compact").with_attribute("attempt", attempt),
536            db_config.clone(),
537            validated_state,
538        )
539        .await
540        .map_err(Error::Database)?;
541        assert_eq!(
542            db.root(),
543            target.root,
544            "validated compact state reconstructed unexpected root",
545        );
546
547        if let Some(callback) = callback {
548            let _ = callback.send(true);
549        }
550        db.persist_compact_state().await?;
551        return Ok(db);
552    }
553}
554
555/// Validate the peer-provided compact state before constructing local database storage.
556fn validate_compact_state<DB>(
557    target: &Target<DB::Family, DB::Digest>,
558    state: State<DB::Family, DB::Op, DB::Digest>,
559) -> CompactFrontierValidation<DB>
560where
561    DB: Database,
562{
563    if state.leaf_count != target.leaf_count {
564        return Err(EngineError::UnexpectedLeafCount {
565            expected: target.leaf_count,
566            actual: state.leaf_count,
567        });
568    }
569
570    let last_commit_loc = Location::new(*state.leaf_count - 1);
571    if !verify_proof::<DB::Hasher, _, _>(
572        &state.last_commit_proof,
573        last_commit_loc,
574        std::slice::from_ref(&state.last_commit_op),
575        &target.root,
576    ) {
577        return Err(EngineError::InvalidProof);
578    }
579
580    validate_compact_frontier::<DB>(target, state)
581}
582
583/// Result of validating a peer-provided compact frontier.
584type CompactFrontierValidation<DB> = Result<
585    ValidatedState<<DB as Database>::Family, <DB as Database>::Op, <DB as Database>::Digest>,
586    EngineError<<DB as Database>::Family, <DB as Database>::Digest>,
587>;
588
589/// Validate that a peer-provided compact frontier authenticates the requested target root.
590fn validate_compact_frontier<DB>(
591    target: &Target<DB::Family, DB::Digest>,
592    state: State<DB::Family, DB::Op, DB::Digest>,
593) -> CompactFrontierValidation<DB>
594where
595    DB: Database,
596{
597    // The final commit is the only operation carried in compact state. Its floor determines which
598    // peaks are inactive when authenticating the compact frontier root.
599    let last_commit_loc = Location::new(*state.leaf_count - 1);
600    let Some(inactivity_floor_loc) = DB::inactivity_floor(&state.last_commit_op) else {
601        return Err(EngineError::InvalidProof);
602    };
603    if inactivity_floor_loc > last_commit_loc {
604        return Err(EngineError::InvalidProof);
605    }
606
607    // Rebuild a disposable Merkle view from the pinned frontier before opening any database
608    // storage. Invalid pin counts or inactive peak layouts are treated as bad peer proofs.
609    let mem = crate::merkle::mem::Mem::<DB::Family, DB::Digest>::init(crate::merkle::mem::Config {
610        nodes: Vec::new(),
611        pruning_boundary: state.leaf_count,
612        pinned_nodes: state.pinned_nodes.clone(),
613    })
614    .map_err(|_| EngineError::InvalidProof)?;
615    let hasher = qmdb::hasher::<DB::Hasher>();
616    let inactive_peaks = DB::Family::inactive_peaks(
617        DB::Family::location_to_position(state.leaf_count),
618        inactivity_floor_loc,
619    );
620    let actual = mem
621        .root(&hasher, inactive_peaks)
622        .map_err(|_| EngineError::InvalidProof)?;
623    if actual != target.root {
624        return Err(EngineError::RootMismatch {
625            expected: target.root,
626            actual,
627        });
628    }
629
630    Ok(ValidatedState {
631        state,
632        root: target.root,
633    })
634}
635
636async fn fetch_state_from_full_source<F, Op, D, Current, Hist, HistFut, Pins, PinsFut>(
637    target: Target<F, D>,
638    current_target: Current,
639    historical_proof: Hist,
640    pinned_nodes_at: Pins,
641) -> Result<State<F, Op, D>, ServeError<F, D>>
642where
643    F: Family,
644    D: Digest,
645    Current: FnOnce() -> Target<F, D>,
646    Hist: FnOnce(Location<F>, Location<F>) -> HistFut,
647    HistFut: Future<Output = Result<(Proof<F, D>, Vec<Op>), qmdb::Error<F>>>,
648    Pins: FnOnce(Location<F>) -> PinsFut,
649    PinsFut: Future<Output = Result<Vec<D>, qmdb::Error<F>>>,
650{
651    // Full sources do not cache a compact witness. Instead, derive the compact payload on demand
652    // from the current tip commit plus the frontier pins at the requested tree size.
653    target.validate().map_err(ServeError::InvalidTarget)?;
654    let current = current_target();
655    if target.root != current.root || target.leaf_count != current.leaf_count {
656        return Err(ServeError::StaleTarget {
657            requested: target,
658            current,
659        });
660    }
661    let leaf_count = target.leaf_count;
662    let last_commit_loc = Location::new(*leaf_count - 1);
663    let (last_commit_proof, mut operations) = historical_proof(leaf_count, last_commit_loc)
664        .await
665        .map_err(ServeError::Database)?;
666    // Compact sync always authenticates exactly the final commit leaf.
667    let last_commit_op =
668        operations
669            .pop()
670            .ok_or(ServeError::Database(qmdb::Error::DataCorrupted(
671                "missing last commit operation",
672            )))?;
673    let pinned_nodes = pinned_nodes_at(leaf_count)
674        .await
675        .map_err(ServeError::Database)?;
676    Ok(State {
677        leaf_count,
678        pinned_nodes,
679        last_commit_op,
680        last_commit_proof,
681    })
682}
683
684// Resolver impls for full keyless databases. These synthesize compact state by querying the
685// historical tip proof and current frontier pins from the full source.
686macro_rules! impl_compact_resolver_keyless {
687    ($db:ident, $op:ident, $val_bound:ident) => {
688        impl<F, E, V, H, S> Resolver for Arc<$db<F, E, V, H, S>>
689        where
690            F: Family,
691            E: crate::Context,
692            V: $val_bound + Send + Sync + 'static,
693            H: Hasher,
694            S: Strategy,
695        {
696            type Family = F;
697            type Digest = H::Digest;
698            type Op = $op<F, V>;
699            type Error = ServeError<F, H::Digest>;
700
701            async fn get_compact_state(
702                &self,
703                target: Target<Self::Family, Self::Digest>,
704            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
705                fetch_state_from_full_source(
706                    target,
707                    || Target::new(self.root(), self.bounds().end),
708                    |leaf_count, last_commit_loc| {
709                        self.historical_proof(
710                            leaf_count,
711                            last_commit_loc,
712                            NonZeroU64::new(1).unwrap(),
713                        )
714                    },
715                    |leaf_count| self.pinned_nodes_at(leaf_count),
716                )
717                .await
718                .map(Into::into)
719            }
720        }
721        impl_compact_resolver_keyless!(@locked $db, $op, $val_bound, AsyncRwLock);
722        impl_compact_resolver_keyless!(@locked $db, $op, $val_bound, TracedAsyncRwLock);
723    };
724    (@locked $db:ident, $op:ident, $val_bound:ident, $lock:ident) => {
725
726        impl<F, E, V, H, S> Resolver for Arc<$lock<$db<F, E, V, H, S>>>
727        where
728            F: Family,
729            E: crate::Context,
730            V: $val_bound + Send + Sync + 'static,
731            H: Hasher,
732            S: Strategy,
733        {
734            type Family = F;
735            type Digest = H::Digest;
736            type Op = $op<F, V>;
737            type Error = ServeError<F, H::Digest>;
738
739            async fn get_compact_state(
740                &self,
741                target: Target<Self::Family, Self::Digest>,
742            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
743                let db = self.read().await;
744                fetch_state_from_full_source(
745                    target,
746                    || Target::new(db.root(), db.bounds().end),
747                    |leaf_count, last_commit_loc| {
748                        db.historical_proof(
749                            leaf_count,
750                            last_commit_loc,
751                            NonZeroU64::new(1).unwrap(),
752                        )
753                    },
754                    |leaf_count| db.pinned_nodes_at(leaf_count),
755                )
756                .await
757                .map(Into::into)
758            }
759        }
760
761        impl<F, E, V, H, S> Resolver for Arc<$lock<Option<$db<F, E, V, H, S>>>>
762        where
763            F: Family,
764            E: crate::Context,
765            V: $val_bound + Send + Sync + 'static,
766            H: Hasher,
767            S: Strategy,
768        {
769            type Family = F;
770            type Digest = H::Digest;
771            type Op = $op<F, V>;
772            type Error = ServeError<F, H::Digest>;
773
774            async fn get_compact_state(
775                &self,
776                target: Target<Self::Family, Self::Digest>,
777            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
778                let guard = self.read().await;
779                let db = guard.as_ref().ok_or(ServeError::MissingSource)?;
780                fetch_state_from_full_source(
781                    target,
782                    || Target::new(db.root(), db.bounds().end),
783                    |leaf_count, last_commit_loc| {
784                        db.historical_proof(
785                            leaf_count,
786                            last_commit_loc,
787                            NonZeroU64::new(1).unwrap(),
788                        )
789                    },
790                    |leaf_count| db.pinned_nodes_at(leaf_count),
791                )
792                .await
793                .map(Into::into)
794            }
795        }
796    };
797}
798
799// Resolver impls for full immutable databases. Same pattern as keyless, but with the extra key and
800// translator parameters carried by immutable variants.
801macro_rules! impl_compact_resolver_immutable {
802    ($db:ident, $op:ident, $val_bound:ident, $key_bound:path) => {
803        impl<F, E, K, V, H, T, S> Resolver for Arc<$db<F, E, K, V, H, T, S>>
804        where
805            F: Family,
806            E: crate::Context,
807            K: $key_bound,
808            V: $val_bound + Send + Sync + 'static,
809            H: Hasher,
810            T: Translator + Send + Sync + 'static,
811            T::Key: Send + Sync,
812            S: Strategy,
813        {
814            type Family = F;
815            type Digest = H::Digest;
816            type Op = $op<F, K, V>;
817            type Error = ServeError<F, H::Digest>;
818
819            async fn get_compact_state(
820                &self,
821                target: Target<Self::Family, Self::Digest>,
822            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
823                fetch_state_from_full_source(
824                    target,
825                    || Target::new(self.root(), self.bounds().end),
826                    |leaf_count, last_commit_loc| {
827                        self.historical_proof(
828                            leaf_count,
829                            last_commit_loc,
830                            NonZeroU64::new(1).unwrap(),
831                        )
832                    },
833                    |leaf_count| self.pinned_nodes_at(leaf_count),
834                )
835                .await
836                .map(Into::into)
837            }
838        }
839        impl_compact_resolver_immutable!(@locked $db, $op, $val_bound, $key_bound, AsyncRwLock);
840        impl_compact_resolver_immutable!(@locked $db, $op, $val_bound, $key_bound, TracedAsyncRwLock);
841    };
842    (@locked $db:ident, $op:ident, $val_bound:ident, $key_bound:path, $lock:ident) => {
843
844        impl<F, E, K, V, H, T, S> Resolver for Arc<$lock<$db<F, E, K, V, H, T, S>>>
845        where
846            F: Family,
847            E: crate::Context,
848            K: $key_bound,
849            V: $val_bound + Send + Sync + 'static,
850            H: Hasher,
851            T: Translator + Send + Sync + 'static,
852            T::Key: Send + Sync,
853            S: Strategy,
854        {
855            type Family = F;
856            type Digest = H::Digest;
857            type Op = $op<F, K, V>;
858            type Error = ServeError<F, H::Digest>;
859
860            async fn get_compact_state(
861                &self,
862                target: Target<Self::Family, Self::Digest>,
863            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
864                let db = self.read().await;
865                fetch_state_from_full_source(
866                    target,
867                    || Target::new(db.root(), db.bounds().end),
868                    |leaf_count, last_commit_loc| {
869                        db.historical_proof(
870                            leaf_count,
871                            last_commit_loc,
872                            NonZeroU64::new(1).unwrap(),
873                        )
874                    },
875                    |leaf_count| db.pinned_nodes_at(leaf_count),
876                )
877                .await
878                .map(Into::into)
879            }
880        }
881
882        impl<F, E, K, V, H, T, S> Resolver for Arc<$lock<Option<$db<F, E, K, V, H, T, S>>>>
883        where
884            F: Family,
885            E: crate::Context,
886            K: $key_bound,
887            V: $val_bound + Send + Sync + 'static,
888            H: Hasher,
889            T: Translator + Send + Sync + 'static,
890            T::Key: Send + Sync,
891            S: Strategy,
892        {
893            type Family = F;
894            type Digest = H::Digest;
895            type Op = $op<F, K, V>;
896            type Error = ServeError<F, H::Digest>;
897
898            async fn get_compact_state(
899                &self,
900                target: Target<Self::Family, Self::Digest>,
901            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
902                let guard = self.read().await;
903                let db = guard.as_ref().ok_or(ServeError::MissingSource)?;
904                fetch_state_from_full_source(
905                    target,
906                    || Target::new(db.root(), db.bounds().end),
907                    |leaf_count, last_commit_loc| {
908                        db.historical_proof(
909                            leaf_count,
910                            last_commit_loc,
911                            NonZeroU64::new(1).unwrap(),
912                        )
913                    },
914                    |leaf_count| db.pinned_nodes_at(leaf_count),
915                )
916                .await
917                .map(Into::into)
918            }
919        }
920    };
921}
922
923// Resolver impls for compact keyless databases. These already persist a compact witness, so serving
924// is just a target check over the current witness rather than reconstructing anything from history.
925macro_rules! impl_compact_resolver_compact_keyless {
926    ($db:ident, $op:ident) => {
927        impl<F, E, V, H, C, S> Resolver for Arc<$db<F, E, V, H, C, S>>
928        where
929            F: Family,
930            E: crate::Context,
931            V: ValueEncoding + Send + Sync + 'static,
932            H: Hasher,
933            $op<F, V>: Encode + Read<Cfg = C>,
934            C: Clone + Send + Sync + 'static,
935            S: Strategy,
936        {
937            type Family = F;
938            type Digest = H::Digest;
939            type Op = $op<F, V>;
940            type Error = ServeError<F, H::Digest>;
941
942            async fn get_compact_state(
943                &self,
944                target: Target<Self::Family, Self::Digest>,
945            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
946                self.compact_state(target).map(Into::into)
947            }
948        }
949        impl_compact_resolver_compact_keyless!(@locked $db, $op, AsyncRwLock);
950        impl_compact_resolver_compact_keyless!(@locked $db, $op, TracedAsyncRwLock);
951    };
952    (@locked $db:ident, $op:ident, $lock:ident) => {
953
954        impl<F, E, V, H, C, S> Resolver for Arc<$lock<$db<F, E, V, H, C, S>>>
955        where
956            F: Family,
957            E: crate::Context,
958            V: ValueEncoding + Send + Sync + 'static,
959            H: Hasher,
960            $op<F, V>: Encode + Read<Cfg = C>,
961            C: Clone + Send + Sync + 'static,
962            S: Strategy,
963        {
964            type Family = F;
965            type Digest = H::Digest;
966            type Op = $op<F, V>;
967            type Error = ServeError<F, H::Digest>;
968
969            async fn get_compact_state(
970                &self,
971                target: Target<Self::Family, Self::Digest>,
972            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
973                let db = self.read().await;
974                db.compact_state(target).map(Into::into)
975            }
976        }
977
978        impl<F, E, V, H, C, S> Resolver for Arc<$lock<Option<$db<F, E, V, H, C, S>>>>
979        where
980            F: Family,
981            E: crate::Context,
982            V: ValueEncoding + Send + Sync + 'static,
983            H: Hasher,
984            $op<F, V>: Encode + Read<Cfg = C>,
985            C: Clone + Send + Sync + 'static,
986            S: Strategy,
987        {
988            type Family = F;
989            type Digest = H::Digest;
990            type Op = $op<F, V>;
991            type Error = ServeError<F, H::Digest>;
992
993            async fn get_compact_state(
994                &self,
995                target: Target<Self::Family, Self::Digest>,
996            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
997                let guard = self.read().await;
998                let db = guard.as_ref().ok_or(ServeError::MissingSource)?;
999                db.compact_state(target).map(Into::into)
1000            }
1001        }
1002    };
1003}
1004
1005// Resolver impls for compact immutable databases. Like the keyless compact path, these read the
1006// persisted witness directly instead of rebuilding it from a full operation log.
1007macro_rules! impl_compact_resolver_compact_immutable {
1008    ($db:ident, $op:ident) => {
1009        impl<F, E, K, V, H, C, S> Resolver for Arc<$db<F, E, K, V, H, C, S>>
1010        where
1011            F: Family,
1012            E: crate::Context,
1013            K: Key,
1014            V: ValueEncoding + Send + Sync + 'static,
1015            H: Hasher,
1016            $op<F, K, V>: Encode + Read<Cfg = C>,
1017            C: Clone + Send + Sync + 'static,
1018            S: Strategy,
1019        {
1020            type Family = F;
1021            type Digest = H::Digest;
1022            type Op = $op<F, K, V>;
1023            type Error = ServeError<F, H::Digest>;
1024
1025            async fn get_compact_state(
1026                &self,
1027                target: Target<Self::Family, Self::Digest>,
1028            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
1029                self.compact_state(target).map(Into::into)
1030            }
1031        }
1032        impl_compact_resolver_compact_immutable!(@locked $db, $op, AsyncRwLock);
1033        impl_compact_resolver_compact_immutable!(@locked $db, $op, TracedAsyncRwLock);
1034    };
1035    (@locked $db:ident, $op:ident, $lock:ident) => {
1036
1037        impl<F, E, K, V, H, C, S> Resolver for Arc<$lock<$db<F, E, K, V, H, C, S>>>
1038        where
1039            F: Family,
1040            E: crate::Context,
1041            K: Key,
1042            V: ValueEncoding + Send + Sync + 'static,
1043            H: Hasher,
1044            $op<F, K, V>: Encode + Read<Cfg = C>,
1045            C: Clone + Send + Sync + 'static,
1046            S: Strategy,
1047        {
1048            type Family = F;
1049            type Digest = H::Digest;
1050            type Op = $op<F, K, V>;
1051            type Error = ServeError<F, H::Digest>;
1052
1053            async fn get_compact_state(
1054                &self,
1055                target: Target<Self::Family, Self::Digest>,
1056            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
1057                let db = self.read().await;
1058                db.compact_state(target).map(Into::into)
1059            }
1060        }
1061
1062        impl<F, E, K, V, H, C, S> Resolver for Arc<$lock<Option<$db<F, E, K, V, H, C, S>>>>
1063        where
1064            F: Family,
1065            E: crate::Context,
1066            K: Key,
1067            V: ValueEncoding + Send + Sync + 'static,
1068            H: Hasher,
1069            $op<F, K, V>: Encode + Read<Cfg = C>,
1070            C: Clone + Send + Sync + 'static,
1071            S: Strategy,
1072        {
1073            type Family = F;
1074            type Digest = H::Digest;
1075            type Op = $op<F, K, V>;
1076            type Error = ServeError<F, H::Digest>;
1077
1078            async fn get_compact_state(
1079                &self,
1080                target: Target<Self::Family, Self::Digest>,
1081            ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
1082                let guard = self.read().await;
1083                let db = guard.as_ref().ok_or(ServeError::MissingSource)?;
1084                db.compact_state(target).map(Into::into)
1085            }
1086        }
1087    };
1088}
1089
1090impl_compact_resolver_compact_keyless!(KeylessCompactDb, KeylessOp);
1091impl_compact_resolver_compact_immutable!(ImmutableCompactDb, ImmutableOp);
1092
1093impl_compact_resolver_keyless!(KeylessFixedDb, KeylessFixedOp, FixedValue);
1094impl_compact_resolver_keyless!(KeylessVariableDb, KeylessVariableOp, VariableValue);
1095impl_compact_resolver_immutable!(ImmutableFixedDb, ImmutableFixedOp, FixedValue, Array);
1096impl_compact_resolver_immutable!(ImmutableVariableDb, ImmutableVariableOp, VariableValue, Key);
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::{Config, Database, FetchResult, Resolver, State, Target};
1101    use crate::{
1102        merkle::{mmr, Location},
1103        qmdb,
1104    };
1105    use commonware_codec::{DecodeExt as _, Encode as _, RangeCfg};
1106    use commonware_cryptography::{sha256::Digest, Hasher as _, Sha256};
1107    use commonware_parallel::Rayon;
1108    use commonware_runtime::{deterministic, Runner as _};
1109    use commonware_utils::sync::AsyncRwLock;
1110    use std::{
1111        collections::VecDeque,
1112        convert::Infallible,
1113        sync::{
1114            atomic::{AtomicUsize, Ordering},
1115            Arc,
1116        },
1117    };
1118
1119    macro_rules! assert_resolver_variants {
1120        ($db:ty) => {
1121            assert_resolver::<Arc<$db>>();
1122            assert_resolver::<Arc<AsyncRwLock<$db>>>();
1123            assert_resolver::<Arc<AsyncRwLock<Option<$db>>>>();
1124        };
1125    }
1126
1127    fn assert_resolver<R: super::Resolver>() {}
1128
1129    struct TestDb {
1130        root: Digest,
1131    }
1132
1133    impl Database for TestDb {
1134        type Family = mmr::Family;
1135        type Op = u8;
1136        type Config = (Digest, Arc<AtomicUsize>);
1137        type Digest = Digest;
1138        type Context = deterministic::Context;
1139        type Hasher = Sha256;
1140
1141        async fn from_validated_state(
1142            _context: Self::Context,
1143            (root, constructions): Self::Config,
1144            _state: super::ValidatedState<Self::Family, Self::Op, Self::Digest>,
1145        ) -> Result<Self, qmdb::Error<Self::Family>> {
1146            constructions.fetch_add(1, Ordering::SeqCst);
1147            Ok(Self { root })
1148        }
1149
1150        fn inactivity_floor(_op: &Self::Op) -> Option<Location<Self::Family>> {
1151            Some(Location::new(0))
1152        }
1153
1154        fn root(&self) -> Self::Digest {
1155            self.root
1156        }
1157
1158        async fn persist_compact_state(&mut self) -> Result<(), qmdb::Error<Self::Family>> {
1159            Ok(())
1160        }
1161    }
1162
1163    #[derive(Clone)]
1164    struct SequenceResolver {
1165        states: Arc<commonware_utils::sync::Mutex<VecDeque<FetchResult<mmr::Family, u8, Digest>>>>,
1166    }
1167
1168    impl Resolver for SequenceResolver {
1169        type Family = mmr::Family;
1170        type Digest = Digest;
1171        type Op = u8;
1172        type Error = Infallible;
1173
1174        async fn get_compact_state(
1175            &self,
1176            _target: Target<Self::Family, Self::Digest>,
1177        ) -> Result<FetchResult<Self::Family, Self::Op, Self::Digest>, Self::Error> {
1178            Ok(self
1179                .states
1180                .lock()
1181                .pop_front()
1182                .expect("missing compact fetch result"))
1183        }
1184    }
1185
1186    fn valid_state_and_target() -> (State<mmr::Family, u8, Digest>, Target<mmr::Family, Digest>) {
1187        let hasher = qmdb::hasher::<Sha256>();
1188        let mut merkle = crate::merkle::mem::Mem::<mmr::Family, Digest>::new();
1189        let op = 0u8;
1190        let first_op = 1u8;
1191        let batch = merkle
1192            .new_batch()
1193            .add(&hasher, &first_op.encode())
1194            .add(&hasher, &op.encode());
1195        let batch = batch.merkleize(&merkle, &hasher);
1196        merkle.apply_batch(&batch).unwrap();
1197        let root = merkle.root(&hasher, 0).unwrap();
1198        let leaf_count = Location::new(2);
1199        let pinned_nodes = merkle
1200            .nodes_to_pin(leaf_count)
1201            .into_values()
1202            .collect::<Vec<_>>();
1203        let proof = merkle.proof(&hasher, Location::new(1), 0).unwrap();
1204        (
1205            State {
1206                leaf_count,
1207                pinned_nodes,
1208                last_commit_op: op,
1209                last_commit_proof: proof,
1210            },
1211            Target::<mmr::Family, Digest> { root, leaf_count },
1212        )
1213    }
1214
1215    #[test]
1216    fn test_all_compact_qmdb_variants_implement_strategy_resolvers() {
1217        type KeylessFixedCompactDb = crate::qmdb::keyless::fixed::CompactDb<
1218            mmr::Family,
1219            deterministic::Context,
1220            Digest,
1221            commonware_cryptography::Sha256,
1222            Rayon,
1223        >;
1224        type KeylessVariableCompactDb = crate::qmdb::keyless::variable::CompactDb<
1225            mmr::Family,
1226            deterministic::Context,
1227            Vec<u8>,
1228            commonware_cryptography::Sha256,
1229            (RangeCfg<usize>, ()),
1230            Rayon,
1231        >;
1232        type ImmutableFixedCompactDb = crate::qmdb::immutable::fixed::CompactDb<
1233            mmr::Family,
1234            deterministic::Context,
1235            Digest,
1236            Digest,
1237            commonware_cryptography::Sha256,
1238            Rayon,
1239        >;
1240        type ImmutableVariableCompactDb = crate::qmdb::immutable::variable::CompactDb<
1241            mmr::Family,
1242            deterministic::Context,
1243            Digest,
1244            Vec<u8>,
1245            commonware_cryptography::Sha256,
1246            ((), (RangeCfg<usize>, ())),
1247            Rayon,
1248        >;
1249
1250        assert_resolver_variants!(KeylessFixedCompactDb);
1251        assert_resolver_variants!(KeylessVariableCompactDb);
1252        assert_resolver_variants!(ImmutableFixedCompactDb);
1253        assert_resolver_variants!(ImmutableVariableCompactDb);
1254    }
1255
1256    #[test]
1257    fn test_target_decode_rejects_zero_leaf_count() {
1258        let unused_root = commonware_cryptography::Sha256::hash(b"unused");
1259        let encoded = Target::<mmr::Family, Digest> {
1260            root: unused_root,
1261            leaf_count: crate::merkle::Location::new(0),
1262        }
1263        .encode();
1264
1265        assert!(Target::<mmr::Family, Digest>::decode(encoded).is_err());
1266    }
1267
1268    #[test]
1269    fn test_compact_sync_retries_invalid_state_without_feedback() {
1270        deterministic::Runner::default().start(|context| async move {
1271            let (good_state, target) = valid_state_and_target();
1272            let mut bad_state = good_state.clone();
1273            bad_state.pinned_nodes.push(Sha256::hash(b"extra pin"));
1274            let (good_tx, good_rx) = commonware_utils::channel::oneshot::channel();
1275            let constructions = Arc::new(AtomicUsize::new(0));
1276
1277            let db = super::sync::<TestDb, _>(Config {
1278                context,
1279                resolver: SequenceResolver {
1280                    states: Arc::new(commonware_utils::sync::Mutex::new(VecDeque::from([
1281                        FetchResult {
1282                            state: bad_state,
1283                            callback: None,
1284                        },
1285                        FetchResult {
1286                            state: good_state,
1287                            callback: Some(good_tx),
1288                        },
1289                    ]))),
1290                },
1291                target: target.clone(),
1292                db_config: (target.root, constructions.clone()),
1293                update_rx: None,
1294                finish_rx: None,
1295                reached_target_tx: None,
1296            })
1297            .await
1298            .unwrap();
1299
1300            assert!(good_rx.await.expect("valid feedback should arrive"));
1301            assert_eq!(constructions.load(Ordering::SeqCst), 1);
1302            assert_eq!(db.root(), target.root);
1303        });
1304    }
1305}
1306
1307#[cfg(all(test, feature = "arbitrary"))]
1308mod conformance {
1309    use super::*;
1310    use crate::merkle::{mmb, mmr};
1311    use commonware_codec::conformance::CodecConformance;
1312    use commonware_cryptography::sha256::Digest as Sha256Digest;
1313
1314    commonware_conformance::conformance_tests! {
1315        CodecConformance<Target<mmr::Family, Sha256Digest>>,
1316        CodecConformance<Target<mmb::Family, Sha256Digest>>,
1317    }
1318}