@@ -160,6 +160,41 @@ impl<
160160 Ok ( result)
161161 }
162162
163+ /// Returns the frontier of the tree, if the tree is nonempty and the frontier
164+ /// can be determined from the available data.
165+ ///
166+ /// The frontier consists of the rightmost leaf and the ommers (sibling hashes)
167+ /// needed to compute the root. This method assembles ommers from within the
168+ /// last shard (below `SHARD_HEIGHT`) and from sibling subtree roots above the
169+ /// shard level.
170+ pub fn frontier ( & self ) -> Result < Option < NonEmptyFrontier < H > > , ShardTreeError < S :: Error > > {
171+ let last_shard = self . store . last_shard ( ) . map_err ( ShardTreeError :: Storage ) ?;
172+ let last_shard = match last_shard {
173+ Some ( s) => s,
174+ None => return Ok ( None ) ,
175+ } ;
176+
177+ let ( position, leaf, mut ommers) = match last_shard. frontier_ommers ( ) {
178+ Some ( parts) => parts,
179+ None => return Ok ( None ) ,
180+ } ;
181+
182+ // Walk from the shard root address up to the tree root, collecting ommers
183+ // from sibling subtrees above the shard level.
184+ let mut cur_addr = last_shard. root_addr ;
185+ while cur_addr. level ( ) < Level :: from ( DEPTH ) {
186+ if cur_addr. is_right_child ( ) {
187+ let sibling_hash = self . root ( cur_addr. sibling ( ) , position + 1 ) ?;
188+ ommers. push ( sibling_hash) ;
189+ }
190+ cur_addr = cur_addr. parent ( ) ;
191+ }
192+
193+ NonEmptyFrontier :: from_parts ( position, leaf, ommers)
194+ . map ( Some )
195+ . map_err ( |_| ShardTreeError :: Query ( QueryError :: TreeIncomplete ( vec ! [ Self :: root_addr( ) ] ) ) )
196+ }
197+
163198 /// Inserts a new root into the tree at the given address.
164199 ///
165200 /// The level associated with the given address may not exceed `DEPTH`.
@@ -1472,6 +1507,110 @@ mod tests {
14721507 check_rewind_remove_mark ( new_tree) ;
14731508 }
14741509
1510+ #[ test]
1511+ fn frontier_empty_tree ( ) {
1512+ let tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1513+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1514+ assert_eq ! ( tree. frontier( ) . unwrap( ) , None ) ;
1515+ }
1516+
1517+ #[ test]
1518+ fn frontier_single_leaf ( ) {
1519+ let mut tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1520+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1521+ tree. append ( "a" . to_string ( ) , Retention :: Ephemeral ) . unwrap ( ) ;
1522+
1523+ let frontier = tree. frontier ( ) . unwrap ( ) . unwrap ( ) ;
1524+ assert_eq ! (
1525+ frontier,
1526+ NonEmptyFrontier :: from_parts( Position :: from( 0 ) , "a" . to_string( ) , vec![ ] ) . unwrap( )
1527+ ) ;
1528+ }
1529+
1530+ #[ test]
1531+ fn frontier_within_single_shard ( ) {
1532+ // Insert a frontier at position 5 within the first shard (SHARD_HEIGHT=3).
1533+ // Position 5 = binary 101: ommers at level 0 ("e") and level 2 ("abcd").
1534+ let original = NonEmptyFrontier :: from_parts (
1535+ Position :: from ( 5 ) ,
1536+ "f" . to_string ( ) ,
1537+ vec ! [ "e" . to_string( ) , "abcd" . to_string( ) ] ,
1538+ )
1539+ . unwrap ( ) ;
1540+
1541+ let mut tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1542+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1543+ tree. insert_frontier_nodes ( original. clone ( ) , Retention :: Ephemeral )
1544+ . unwrap ( ) ;
1545+
1546+ let frontier = tree. frontier ( ) . unwrap ( ) . unwrap ( ) ;
1547+ assert_eq ! ( frontier, original) ;
1548+ }
1549+
1550+ #[ test]
1551+ fn frontier_multi_shard ( ) {
1552+ // Insert a frontier at position 9, which is in the second shard (SHARD_HEIGHT=3).
1553+ // Position 9 = binary 1001: ommers at level 0 ("i") and level 3 ("abcdefgh").
1554+ // Level 0 ommer "i" is within the shard; level 3 ommer "abcdefgh" is the
1555+ // sibling shard's root above the shard level.
1556+ let original = NonEmptyFrontier :: from_parts (
1557+ Position :: from ( 9 ) ,
1558+ "j" . to_string ( ) ,
1559+ vec ! [ "i" . to_string( ) , "abcdefgh" . to_string( ) ] ,
1560+ )
1561+ . unwrap ( ) ;
1562+
1563+ let mut tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1564+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1565+ tree. insert_frontier_nodes ( original. clone ( ) , Retention :: Ephemeral )
1566+ . unwrap ( ) ;
1567+
1568+ let frontier = tree. frontier ( ) . unwrap ( ) . unwrap ( ) ;
1569+ assert_eq ! ( frontier, original) ;
1570+ }
1571+
1572+ #[ test]
1573+ fn frontier_from_appended_leaves ( ) {
1574+ // Append leaves with the last one marked, so it isn't pruned away.
1575+ let mut tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1576+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1577+ for c in 'a' ..='i' {
1578+ tree. append ( c. to_string ( ) , Retention :: Ephemeral ) . unwrap ( ) ;
1579+ }
1580+ tree. append ( "j" . to_string ( ) , Retention :: Marked ) . unwrap ( ) ;
1581+
1582+ let frontier = tree. frontier ( ) . unwrap ( ) . unwrap ( ) ;
1583+ // Position 9 = binary 1001: ommers at level 0 ("i") and level 3 ("abcdefgh")
1584+ assert_eq ! (
1585+ frontier,
1586+ NonEmptyFrontier :: from_parts(
1587+ Position :: from( 9 ) ,
1588+ "j" . to_string( ) ,
1589+ vec![ "i" . to_string( ) , "abcdefgh" . to_string( ) ]
1590+ )
1591+ . unwrap( )
1592+ ) ;
1593+ }
1594+
1595+ #[ test]
1596+ fn frontier_roundtrip ( ) {
1597+ // Build a frontier, insert it, then extract it back.
1598+ let original = NonEmptyFrontier :: from_parts (
1599+ Position :: from ( 9 ) ,
1600+ "j" . to_string ( ) ,
1601+ vec ! [ "i" . to_string( ) , "abcdefgh" . to_string( ) ] ,
1602+ )
1603+ . unwrap ( ) ;
1604+
1605+ let mut tree: ShardTree < MemoryShardStore < String , u32 > , 4 , 3 > =
1606+ ShardTree :: new ( MemoryShardStore :: empty ( ) , 100 ) ;
1607+ tree. insert_frontier_nodes ( original. clone ( ) , Retention :: Ephemeral )
1608+ . unwrap ( ) ;
1609+
1610+ let extracted = tree. frontier ( ) . unwrap ( ) . unwrap ( ) ;
1611+ assert_eq ! ( extracted, original) ;
1612+ }
1613+
14751614 #[ test]
14761615 fn checkpoint_pruning_repeated ( ) {
14771616 // Create a tree with some leaves.
0 commit comments