@@ -13,7 +13,7 @@ use crate::{
1313 contiguous:: { Contiguous , Mutable , Reader } ,
1414 Error as JournalError ,
1515 } ,
16- merkle:: { Family , Location , Proof } ,
16+ merkle:: { Bagging , Family , Location , Proof , RootSpec } ,
1717 qmdb:: {
1818 bitmap:: Shared , build_snapshot_from_log, delete_known_loc,
1919 operation:: Operation as OperationTrait , update_known_loc, Error ,
@@ -73,6 +73,15 @@ pub struct Db<
7373 /// - There is always at least one commit operation in the log.
7474 pub ( crate ) log : AuthenticatedLog < F , E , C , H > ,
7575
76+ /// Cached operations root for this database.
77+ pub ( crate ) root : H :: Digest ,
78+
79+ /// Whether the operations root uses inactive-prefix split semantics.
80+ pub ( crate ) split_root : bool ,
81+
82+ /// Bagging used by the operations root.
83+ pub ( crate ) root_bagging : Bagging ,
84+
7685 /// A location before which all operations are "inactive" (that is, operations before this point
7786 /// are over keys that have been updated by some operation at or after this point).
7887 pub ( crate ) inactivity_floor_loc : Location < F > ,
@@ -145,8 +154,15 @@ where
145154 }
146155 }
147156
148- pub fn root ( & self ) -> H :: Digest {
149- self . log . root ( )
157+ /// Return the canonical QMDB operations root.
158+ pub const fn root ( & self ) -> H :: Digest {
159+ self . root
160+ }
161+
162+ /// Return the operations-root spec for the given leaf count and inactivity floor.
163+ pub ( crate ) fn root_spec ( & self , leaves : Location < F > , inactivity_floor : Location < F > ) -> RootSpec {
164+ let inactive_peaks = F :: inactive_peaks ( F :: location_to_position ( leaves) , inactivity_floor) ;
165+ RootSpec :: from_split_policy ( self . split_root , self . root_bagging , inactive_peaks)
150166 }
151167
152168 /// Get the value of `key` in the db, or None if it has no value.
@@ -302,14 +318,46 @@ where
302318 Ok ( ( ) )
303319 }
304320
321+ /// Returns a historical proof for `historical_size` operations, anchored at `start_loc`
322+ /// and bounded by `max_ops`.
323+ ///
324+ /// # Contract
325+ ///
326+ /// When this database is configured with `split_root: true`, `historical_size` must be a
327+ /// commit-boundary size: the operation at `historical_size - 1` must itself be a commit
328+ /// op declaring the governing inactivity floor. With `split_root: false` the spec does
329+ /// not depend on the floor and any retained `historical_size` works.
330+ ///
331+ /// # Errors
332+ ///
333+ /// Returns [`crate::qmdb::Error::HistoricalFloorPruned`] (split_root only) if
334+ /// `historical_size - 1` is retained but is not a commit op, either because the caller
335+ /// passed a non-commit-boundary size or because pruning removed the commit that would
336+ /// have governed it.
305337 pub async fn historical_proof (
306338 & self ,
307339 historical_size : Location < F > ,
308340 start_loc : Location < F > ,
309341 max_ops : NonZeroU64 ,
310342 ) -> Result < ( Proof < F , H :: Digest > , Vec < Operation < F , U > > ) , crate :: qmdb:: Error < F > > {
343+ if historical_size > self . log . size ( ) . await {
344+ return Err ( crate :: qmdb:: Error :: Merkle (
345+ crate :: merkle:: Error :: RangeOutOfBounds ( historical_size) ,
346+ ) ) ;
347+ }
348+
349+ let inactivity_floor = if self . split_root {
350+ let reader = self . log . reader ( ) . await ;
351+ crate :: qmdb:: find_inactivity_floor_at :: < F , _ > ( & reader, historical_size, |op| {
352+ op. has_floor ( )
353+ } )
354+ . await ?
355+ } else {
356+ Location :: new ( 0 )
357+ } ;
358+ let spec = self . root_spec ( historical_size, inactivity_floor) ;
311359 self . log
312- . historical_proof ( historical_size, start_loc, max_ops)
360+ . historical_proof ( historical_size, start_loc, max_ops, spec )
313361 . await
314362 . map_err ( Into :: into)
315363 }
@@ -484,6 +532,9 @@ where
484532 ) ) ?;
485533 self . last_commit_loc = Location :: new ( rewind_size - 1 ) ;
486534 self . inactivity_floor_loc = rewind_floor;
535+ self . root = self
536+ . log
537+ . root ( self . root_spec ( Location :: new ( rewind_size) , rewind_floor) ) ?;
487538
488539 Ok ( ( ) )
489540 }
@@ -512,13 +563,20 @@ where
512563 mut index : I ,
513564 log : AuthenticatedLog < F , E , C , H > ,
514565 shared_bitmap : Option < Arc < Shared < N > > > ,
566+ split_root : bool ,
567+ root_bagging : Bagging ,
515568 ) -> Result < Self , crate :: qmdb:: Error < F > > {
516569 let ( last_commit_loc, inactivity_floor_loc, active_keys, bitmap) = {
517570 let reader = log. reader ( ) . await ;
518571 let bounds = reader. bounds ( ) ;
519572 let last_commit_loc = bounds. end . checked_sub ( 1 ) . expect ( "commit should exist" ) ;
520573 let last_commit = reader. read ( last_commit_loc) . await ?;
521574 let inactivity_floor_loc = last_commit. has_floor ( ) . expect ( "should be a commit" ) ;
575+ if * inactivity_floor_loc > last_commit_loc {
576+ return Err ( crate :: qmdb:: Error :: DataCorrupted (
577+ "inactivity floor exceeds last commit" ,
578+ ) ) ;
579+ }
522580
523581 // Seed the bitmap so its pruned prefix matches the retained log boundary. Bits in
524582 // [pruned_bits, bounds.start) correspond to pruned operations and remain 0; replay
@@ -585,8 +643,21 @@ where
585643 ) ) ;
586644 }
587645
646+ let inactive_peaks = F :: inactive_peaks (
647+ F :: location_to_position ( log. merkle . leaves ( ) ) ,
648+ inactivity_floor_loc,
649+ ) ;
650+ let root = log. root ( RootSpec :: from_split_policy (
651+ split_root,
652+ root_bagging,
653+ inactive_peaks,
654+ ) ) ?;
655+
588656 Ok ( Self {
589657 log,
658+ root,
659+ split_root,
660+ root_bagging,
590661 inactivity_floor_loc,
591662 snapshot : index,
592663 last_commit_loc,
0 commit comments