@@ -39,7 +39,8 @@ use generic_log_worker::{
3939} ;
4040use length_prefixed:: { ReadLengthPrefixedBytesExt as _, WriteLengthPrefixedBytesExt as _} ;
4141use tlog_core:: {
42- Hash , HashReader as _, TlogError , record_hash, stored_hash_index, stored_hashes_for_record_hash,
42+ EMPTY_HASH , Hash , HashReader , TlogError , record_hash, stored_hash_index,
43+ stored_hashes_for_record_hash,
4344} ;
4445use tlog_tiles:: { PathElem , PreloadedTlogTileReader , TileHashReader , TlogTile , TlogTileRecorder } ;
4546#[ allow( clippy:: wildcard_imports) ]
@@ -162,6 +163,26 @@ async fn authenticated_leaf_hashes(
162163 tree_hash : Hash ,
163164 indexes : & [ u64 ] ,
164165) -> Result < Vec < Hash > > {
166+ let tiles = fetch_authenticated_tiles ( object, tree_size, indexes) . await ?;
167+ let reader = PreloadedTlogTileReader ( tiles) ;
168+ TileHashReader :: new ( tree_size, tree_hash, & reader)
169+ . read_hashes ( indexes)
170+ . map_err ( |e| Error :: from ( format ! ( "authenticate persisted leaf hashes: {e:?}" ) ) )
171+ }
172+
173+ /// Fetch (from storage) the hash tiles needed to prove `indexes` against
174+ /// `(tree_size, tree_hash)`. The returned map feeds a [`TileHashReader`] whose
175+ /// verifying pass authenticates them; a missing or stale tile surfaces as an
176+ /// error rather than a downstream client rejection.
177+ ///
178+ /// # Errors
179+ ///
180+ /// Returns an error if a required hash tile is missing from storage.
181+ async fn fetch_authenticated_tiles (
182+ object : & impl ObjectBackend ,
183+ tree_size : u64 ,
184+ indexes : & [ u64 ] ,
185+ ) -> Result < HashMap < TlogTile , Vec < u8 > > > {
165186 // Recording pass: `TlogTileRecorder` short-circuits `read_hashes` with
166187 // `RecordedTilesOnly` after collecting the tiles it would need.
167188 let recorder = TlogTileRecorder :: default ( ) ;
@@ -184,12 +205,7 @@ async fn authenticated_leaf_hashes(
184205 Ok :: < ( TlogTile , Vec < u8 > ) , Error > ( ( tile, bytes) )
185206 }
186207 } ) ;
187- let tiles: HashMap < TlogTile , Vec < u8 > > = try_join_all ( tile_futures) . await ?. into_iter ( ) . collect ( ) ;
188-
189- let reader = PreloadedTlogTileReader ( tiles) ;
190- TileHashReader :: new ( tree_size, tree_hash, & reader)
191- . read_hashes ( indexes)
192- . map_err ( |e| Error :: from ( format ! ( "authenticate persisted leaf hashes: {e:?}" ) ) )
208+ Ok ( try_join_all ( tile_futures) . await ?. into_iter ( ) . collect ( ) )
193209}
194210
195211/// Authenticate a reloaded partial entry bundle before it is extended and
@@ -435,6 +451,89 @@ pub(crate) async fn write_checkpoint(object: &impl ObjectBackend, bytes: Vec<u8>
435451 . await
436452}
437453
454+ /// Ensure the partial "cut" tiles for a tree of size `cut_size` exist, so a
455+ /// checkpoint at `cut_size` is servable per [tlog-tiles][tiles].
456+ ///
457+ /// Needed when the persisted frontier advanced past `cut_size` (a prior
458+ /// upload, or a racing client): the frontier wrote wider partial tiles, so the
459+ /// narrower ones `cut_size` requires (the level-0 data bundle when mid-tile,
460+ /// and upper-level partial hash tiles) may be missing.
461+ ///
462+ /// Re-persists the leaves `[base, cut_size)`, `base` being the largest
463+ /// 256-boundary below `cut_size`, on top of the aligned prefix, which rewrites
464+ /// exactly the edge tiles at `cut_size`. Re-uploads are idempotent, so racing
465+ /// a concurrent commit is safe. `frontier_size`/`frontier_hash` locate and
466+ /// authenticate the wider stored bundle and the aligned prefix root.
467+ ///
468+ /// # Errors
469+ ///
470+ /// Returns an error on a storage fault, or if the reloaded leaves fail
471+ /// authentication.
472+ pub ( crate ) async fn ensure_cut_tiles (
473+ object : & impl ObjectBackend ,
474+ cut_size : u64 ,
475+ frontier_size : u64 ,
476+ frontier_hash : Hash ,
477+ ) -> Result < ( ) > {
478+ if cut_size == 0 {
479+ return Ok ( ( ) ) ;
480+ }
481+ let base = ( ( cut_size - 1 ) / TILE_WIDTH ) * TILE_WIDTH ;
482+
483+ // Reload the cut leaves from the wider stored bundle, authenticated
484+ // against the frontier tiles.
485+ let leaves =
486+ read_persisted_leaves ( object, base, cut_size - base, frontier_size, frontier_hash) . await ?;
487+
488+ // Root of the aligned prefix, the base persist_entries grows from,
489+ // authenticated against the frontier tree (whose edge tiles exist).
490+ let prefix_hash = if base == 0 {
491+ EMPTY_HASH
492+ } else {
493+ subtree_prefix_hash ( object, base, frontier_size, frontier_hash) . await ?
494+ } ;
495+
496+ persist_entries ( object, base, prefix_hash, cut_size, & leaves) . await ?;
497+ Ok ( ( ) )
498+ }
499+
500+ /// Tree hash of the prefix `[0, prefix_size)`, where `prefix_size` is
501+ /// 256-aligned, read from the full hash tiles authenticated against the target
502+ /// `(tree_size, tree_hash)`.
503+ async fn subtree_prefix_hash (
504+ object : & impl ObjectBackend ,
505+ prefix_size : u64 ,
506+ tree_size : u64 ,
507+ tree_hash : Hash ,
508+ ) -> Result < Hash > {
509+ let indexes = tlog_core:: tree_hash_indexes ( prefix_size) ;
510+ let tiles = fetch_authenticated_tiles ( object, tree_size, & indexes) . await ?;
511+ let reader = PreloadedTlogTileReader ( tiles) ;
512+ let hashes = TileHashReader :: new ( tree_size, tree_hash, & reader)
513+ . read_hashes ( & indexes)
514+ . map_err ( |e| Error :: from ( format ! ( "read prefix hashes: {e:?}" ) ) ) ?;
515+ tlog_core:: tree_hash ( prefix_size, & StaticHashReader ( & indexes, & hashes) )
516+ . map_err ( |e| Error :: from ( format ! ( "compute prefix hash: {e:?}" ) ) )
517+ }
518+
519+ /// A [`HashReader`] over a fixed index-to-hash mapping, for computing a tree
520+ /// hash from a preauthenticated set of stored hashes.
521+ struct StaticHashReader < ' a > ( & ' a [ u64 ] , & ' a [ Hash ] ) ;
522+ impl HashReader for StaticHashReader < ' _ > {
523+ fn read_hashes ( & self , indexes : & [ u64 ] ) -> std:: result:: Result < Vec < Hash > , TlogError > {
524+ indexes
525+ . iter ( )
526+ . map ( |want| {
527+ self . 0
528+ . iter ( )
529+ . position ( |i| i == want)
530+ . map ( |p| self . 1 [ p] )
531+ . ok_or ( TlogError :: IndexesNotInTree )
532+ } )
533+ . collect ( )
534+ }
535+ }
536+
438537/// Upload one entry bundle (data tile). `n` is the tree size after the
439538/// bundle's last entry, so the bundle covers leaves ending at `n - 1`.
440539async fn upload_entry_bundle ( object : & impl ObjectBackend , n : u64 , bytes : Vec < u8 > ) -> Result < ( ) > {
@@ -711,6 +810,95 @@ mod tests {
711810 ) ;
712811 }
713812
813+ /// Assert a checkpoint at `size` is servable: the stored tiles
814+ /// authenticate the last leaf against `reference_root(size)`, and the cut
815+ /// data bundle decodes to the expected leaves.
816+ async fn assert_servable_at ( obj : & MemBackend , size : u64 ) {
817+ use length_prefixed:: ReadLengthPrefixedBytesExt as _;
818+ let root = reference_root ( size) ;
819+
820+ let idx = [ stored_hash_index ( 0 , size - 1 ) ] ;
821+ let tiles = fetch_authenticated_tiles ( obj, size, & idx) . await . unwrap ( ) ;
822+ let reader = PreloadedTlogTileReader ( tiles) ;
823+ let got = TileHashReader :: new ( size, root, & reader)
824+ . read_hashes ( & idx)
825+ . expect ( "tiles authenticate against root" ) ;
826+ assert_eq ! ( got[ 0 ] , record_hash( & entry( size - 1 ) ) ) ;
827+
828+ // For a mid-tile size, the cut data bundle must decode to exactly the
829+ // trailing leaves [subtree_start, size).
830+ if !size. is_multiple_of ( TILE_WIDTH ) {
831+ let subtree_start = ( size / TILE_WIDTH ) * TILE_WIDTH ;
832+ let data_key = TlogTile :: from_index ( stored_hash_index ( 0 , size - 1 ) )
833+ . with_data_path ( PathElem :: Entries )
834+ . path ( ) ;
835+ let bytes = obj
836+ . fetch ( & data_key)
837+ . await
838+ . unwrap ( )
839+ . expect ( "cut data bundle stored" ) ;
840+ let mut cur: & [ u8 ] = & bytes;
841+ for i in subtree_start..size {
842+ let got = cur. read_length_prefixed ( 2 ) . unwrap ( ) ;
843+ assert_eq ! ( got, entry( i) , "leaf {i} mismatch in cut bundle" ) ;
844+ }
845+ assert ! ( cur. is_empty( ) , "cut bundle has trailing bytes" ) ;
846+ }
847+ }
848+
849+ #[ tokio:: test]
850+ async fn ensure_cut_tiles_first_block ( ) {
851+ let obj = MemBackend :: default ( ) ;
852+ // Frontier at 300; a checkpoint at 100 (mid-tile, first block) needs
853+ // its width-100 data bundle synthesized. The frontier only wrote the
854+ // width-300 partial (a full width-256 tile 0 + width-44 tile 1).
855+ let frontier = persist_entries ( & obj, 0 , EMPTY_HASH , 300 , & leaves ( 0 ..300 ) )
856+ . await
857+ . unwrap ( ) ;
858+ ensure_cut_tiles ( & obj, 100 , 300 , frontier) . await . unwrap ( ) ;
859+ assert_servable_at ( & obj, 100 ) . await ;
860+ }
861+
862+ #[ tokio:: test]
863+ async fn ensure_cut_tiles_aligned_below_frontier ( ) {
864+ let obj = MemBackend :: default ( ) ;
865+ // Frontier at 768; a checkpoint at a tile-aligned 256 still needs the
866+ // upper-level partial hash tile (tile/1/000.p/1) synthesized: the
867+ // frontier wrote tile/1/000.p/3, never the .p/1 a tree of 256 needs.
868+ let frontier = persist_entries ( & obj, 0 , EMPTY_HASH , 768 , & leaves ( 0 ..768 ) )
869+ . await
870+ . unwrap ( ) ;
871+ ensure_cut_tiles ( & obj, 256 , 768 , frontier) . await . unwrap ( ) ;
872+ assert_servable_at ( & obj, 256 ) . await ;
873+ }
874+
875+ #[ tokio:: test]
876+ async fn ensure_cut_tiles_later_block ( ) {
877+ let obj = MemBackend :: default ( ) ;
878+ // Frontier at 1000; a checkpoint at 900 cuts a later block (base
879+ // 768), exercising the prefix-hash path.
880+ let frontier = persist_entries ( & obj, 0 , EMPTY_HASH , 1000 , & leaves ( 0 ..1000 ) )
881+ . await
882+ . unwrap ( ) ;
883+ ensure_cut_tiles ( & obj, 900 , 1000 , frontier) . await . unwrap ( ) ;
884+ assert_servable_at ( & obj, 900 ) . await ;
885+ }
886+
887+ #[ tokio:: test]
888+ async fn ensure_cut_tiles_rejects_wrong_frontier_hash ( ) {
889+ let obj = MemBackend :: default ( ) ;
890+ persist_entries ( & obj, 0 , EMPTY_HASH , 300 , & leaves ( 0 ..300 ) )
891+ . await
892+ . unwrap ( ) ;
893+ // A frontier hash that doesn't match storage must fail authentication
894+ // rather than write mismatched tiles.
895+ assert ! (
896+ ensure_cut_tiles( & obj, 100 , 300 , Hash ( [ 0xab ; tlog_core:: HASH_SIZE ] ) )
897+ . await
898+ . is_err( )
899+ ) ;
900+ }
901+
714902 #[ tokio:: test]
715903 async fn hash_tiles_authenticate_against_root ( ) {
716904 let obj = MemBackend :: default ( ) ;
0 commit comments