@@ -1027,6 +1027,55 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
10271027 go ( & self . root , self . root_addr , & mut result) ;
10281028 result
10291029 }
1030+
1031+ /// Returns the Merkle frontier of the tree, if the tree is nonempty and has no `Nil` leaves
1032+ /// prior to the leaf at the greatest position.
1033+ pub fn frontier ( & self ) -> Option < NonEmptyFrontier < H > > {
1034+ /// Traverses the rightmost path of the tree, collecting the frontier leaf and ommers.
1035+ /// Returns `(position, leaf_hash, ommers)` with ommers ordered from lowest to highest
1036+ /// level, or `None` if the frontier cannot be extracted.
1037+ ///
1038+ /// Pre-condition: `addr` must be the address of `root`.
1039+ fn go < H : Hashable + Clone + PartialEq > (
1040+ addr : Address ,
1041+ root : & PrunableTree < H > ,
1042+ ) -> Option < ( Position , H , Vec < H > ) > {
1043+ match & root. 0 {
1044+ Node :: Nil => None ,
1045+ Node :: Leaf { value : ( h, _) } => {
1046+ if addr. level ( ) == Level :: from ( 0 ) {
1047+ Some ( ( Position :: from ( addr. index ( ) ) , h. clone ( ) , vec ! [ ] ) )
1048+ } else {
1049+ // A leaf above level 0 is a pruned subtree; we cannot extract
1050+ // the frontier leaf or internal ommers.
1051+ None
1052+ }
1053+ }
1054+ Node :: Parent { left, right, .. } => {
1055+ let ( l_addr, r_addr) = addr
1056+ . children ( )
1057+ . expect ( "A parent node cannot appear at level 0" ) ;
1058+
1059+ if right. 0 . is_nil ( ) {
1060+ // Right subtree is empty; the frontier is in the left subtree
1061+ // and no ommer is needed at this level.
1062+ go ( l_addr, left)
1063+ } else {
1064+ // Right subtree is populated; the frontier is within it.
1065+ // The left subtree's root hash becomes an ommer.
1066+ let ( pos, leaf, mut ommers) = go ( r_addr, right) ?;
1067+ let left_hash =
1068+ left. root_hash ( l_addr, r_addr. position_range_start ( ) ) . ok ( ) ?;
1069+ ommers. push ( left_hash) ;
1070+ Some ( ( pos, leaf, ommers) )
1071+ }
1072+ }
1073+ }
1074+ }
1075+
1076+ let ( position, leaf, ommers) = go ( self . root_addr , & self . root ) ?;
1077+ NonEmptyFrontier :: from_parts ( position, leaf, ommers) . ok ( )
1078+ }
10301079}
10311080
10321081// We need an applicative functor for Result for this function so that we can correctly
@@ -1051,7 +1100,7 @@ fn accumulate_result_with<A, B, C>(
10511100mod tests {
10521101 use std:: collections:: { BTreeMap , BTreeSet } ;
10531102
1054- use incrementalmerkletree:: { Address , Level , Position } ;
1103+ use incrementalmerkletree:: { frontier :: NonEmptyFrontier , Address , Level , Position } ;
10551104 use proptest:: proptest;
10561105
10571106 use super :: { LocatedPrunableTree , PrunableTree , RetentionFlags } ;
@@ -1279,6 +1328,230 @@ mod tests {
12791328 ) ;
12801329 }
12811330
1331+ #[ test]
1332+ fn frontier_empty_tree ( ) {
1333+ let t: LocatedPrunableTree < String > = LocatedTree {
1334+ root_addr : Address :: from_parts ( 2 . into ( ) , 0 ) ,
1335+ root : nil ( ) ,
1336+ } ;
1337+ assert_eq ! ( t. frontier( ) , None ) ;
1338+ }
1339+
1340+ #[ test]
1341+ fn frontier_single_leaf ( ) {
1342+ // A single leaf at position 0
1343+ let t: LocatedPrunableTree < String > = LocatedTree {
1344+ root_addr : Address :: from_parts ( 0 . into ( ) , 0 ) ,
1345+ root : leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1346+ } ;
1347+ assert_eq ! (
1348+ t. frontier( ) ,
1349+ Some ( NonEmptyFrontier :: from_parts( Position :: from( 0 ) , "a" . to_string( ) , vec![ ] ) . unwrap( ) )
1350+ ) ;
1351+ }
1352+
1353+ #[ test]
1354+ fn frontier_two_leaves ( ) {
1355+ // Positions 0 and 1 populated
1356+ let t: LocatedPrunableTree < String > = LocatedTree {
1357+ root_addr : Address :: from_parts ( 1 . into ( ) , 0 ) ,
1358+ root : parent (
1359+ leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1360+ leaf ( ( "b" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1361+ ) ,
1362+ } ;
1363+ // Position 1 = binary 1: ommer at level 0 = "a"
1364+ assert_eq ! (
1365+ t. frontier( ) ,
1366+ Some (
1367+ NonEmptyFrontier :: from_parts(
1368+ Position :: from( 1 ) ,
1369+ "b" . to_string( ) ,
1370+ vec![ "a" . to_string( ) ]
1371+ )
1372+ . unwrap( )
1373+ )
1374+ ) ;
1375+ }
1376+
1377+ #[ test]
1378+ fn frontier_left_only ( ) {
1379+ // Only left child populated (position 0), right is Nil
1380+ let t: LocatedPrunableTree < String > = LocatedTree {
1381+ root_addr : Address :: from_parts ( 1 . into ( ) , 0 ) ,
1382+ root : parent ( leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) , nil ( ) ) ,
1383+ } ;
1384+ // Position 0, no ommers
1385+ assert_eq ! (
1386+ t. frontier( ) ,
1387+ Some ( NonEmptyFrontier :: from_parts( Position :: from( 0 ) , "a" . to_string( ) , vec![ ] ) . unwrap( ) )
1388+ ) ;
1389+ }
1390+
1391+ #[ test]
1392+ fn frontier_deeper_tree ( ) {
1393+ // Positions 0-5 populated at level 3
1394+ // root
1395+ // / \
1396+ // (l2,0) (l2,1)
1397+ // / \ / \
1398+ // ab cd ef Nil
1399+ let t: LocatedPrunableTree < String > = LocatedTree {
1400+ root_addr : Address :: from_parts ( 3 . into ( ) , 0 ) ,
1401+ root : parent (
1402+ parent (
1403+ parent (
1404+ leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1405+ leaf ( ( "b" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1406+ ) ,
1407+ parent (
1408+ leaf ( ( "c" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1409+ leaf ( ( "d" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1410+ ) ,
1411+ ) ,
1412+ parent (
1413+ parent (
1414+ leaf ( ( "e" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1415+ leaf ( ( "f" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1416+ ) ,
1417+ nil ( ) ,
1418+ ) ,
1419+ ) ,
1420+ } ;
1421+
1422+ // Position 5 = binary 101: ommers at levels 0 and 2
1423+ // Level 0 ommer: "e" (sibling of "f")
1424+ // Level 2 ommer: hash of left subtree = "abcd"
1425+ assert_eq ! (
1426+ t. frontier( ) ,
1427+ Some (
1428+ NonEmptyFrontier :: from_parts(
1429+ Position :: from( 5 ) ,
1430+ "f" . to_string( ) ,
1431+ vec![ "e" . to_string( ) , "abcd" . to_string( ) ]
1432+ )
1433+ . unwrap( )
1434+ )
1435+ ) ;
1436+ }
1437+
1438+ #[ test]
1439+ fn frontier_with_pruned_left_sibling ( ) {
1440+ // Left subtree is a pruned leaf (at level > 0), right subtree has detail
1441+ // root (level 3)
1442+ // / \
1443+ // "abcd" (l2,1)
1444+ // (pruned) / \
1445+ // ef Nil
1446+ let t: LocatedPrunableTree < String > = LocatedTree {
1447+ root_addr : Address :: from_parts ( 3 . into ( ) , 0 ) ,
1448+ root : parent (
1449+ leaf ( ( "abcd" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1450+ parent (
1451+ parent (
1452+ leaf ( ( "e" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1453+ leaf ( ( "f" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1454+ ) ,
1455+ nil ( ) ,
1456+ ) ,
1457+ ) ,
1458+ } ;
1459+
1460+ // The pruned left sibling's hash "abcd" should be used as the level-2 ommer
1461+ assert_eq ! (
1462+ t. frontier( ) ,
1463+ Some (
1464+ NonEmptyFrontier :: from_parts(
1465+ Position :: from( 5 ) ,
1466+ "f" . to_string( ) ,
1467+ vec![ "e" . to_string( ) , "abcd" . to_string( ) ]
1468+ )
1469+ . unwrap( )
1470+ )
1471+ ) ;
1472+ }
1473+
1474+ #[ test]
1475+ fn frontier_with_nil_left_sibling ( ) {
1476+ // Left subtree is Nil (incomplete), right has the frontier
1477+ let t: LocatedPrunableTree < String > = LocatedTree {
1478+ root_addr : Address :: from_parts ( 2 . into ( ) , 0 ) ,
1479+ root : parent (
1480+ nil ( ) ,
1481+ parent (
1482+ leaf ( ( "c" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1483+ leaf ( ( "d" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1484+ ) ,
1485+ ) ,
1486+ } ;
1487+
1488+ // Should return None because the left sibling is Nil (incomplete)
1489+ assert_eq ! ( t. frontier( ) , None ) ;
1490+ }
1491+
1492+ #[ test]
1493+ fn frontier_pruned_rightmost_subtree ( ) {
1494+ // Rightmost path hits a pruned leaf at level > 0
1495+ let t: LocatedPrunableTree < String > = LocatedTree {
1496+ root_addr : Address :: from_parts ( 2 . into ( ) , 0 ) ,
1497+ root : parent (
1498+ parent (
1499+ leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1500+ leaf ( ( "b" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1501+ ) ,
1502+ leaf ( ( "cd" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1503+ ) ,
1504+ } ;
1505+
1506+ // Right child is a leaf at level 1 (pruned subtree); can't extract frontier
1507+ assert_eq ! ( t. frontier( ) , None ) ;
1508+ }
1509+
1510+ #[ test]
1511+ fn frontier_nonzero_index_shard ( ) {
1512+ // A shard at index 1 (positions 4-7): the absolute position has bits
1513+ // above the shard level, so NonEmptyFrontier::from_parts will fail.
1514+ let t: LocatedPrunableTree < String > = LocatedTree {
1515+ root_addr : Address :: from_parts ( 2 . into ( ) , 1 ) ,
1516+ root : parent (
1517+ parent (
1518+ leaf ( ( "a" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1519+ leaf ( ( "b" . to_string ( ) , RetentionFlags :: EPHEMERAL ) ) ,
1520+ ) ,
1521+ nil ( ) ,
1522+ ) ,
1523+ } ;
1524+
1525+ // Position 5 (binary 101) needs 2 ommers, but we only have 1 within the shard
1526+ // (level 0 ommer "a"). The bit at level 2 is set in position 5, requiring an
1527+ // ommer we don't have. from_parts rejects this.
1528+ assert_eq ! ( t. frontier( ) , None ) ;
1529+ }
1530+
1531+ #[ test]
1532+ fn frontier_roundtrip_via_insert ( ) {
1533+ use incrementalmerkletree:: Retention ;
1534+
1535+ // Build a frontier, insert it into an empty tree, then extract it back
1536+ let frontier = NonEmptyFrontier :: from_parts (
1537+ Position :: from ( 5 ) ,
1538+ "f" . to_string ( ) ,
1539+ vec ! [ "e" . to_string( ) , "abcd" . to_string( ) ] ,
1540+ )
1541+ . unwrap ( ) ;
1542+
1543+ let empty_tree: LocatedPrunableTree < String > = LocatedTree {
1544+ root_addr : Address :: from_parts ( 3 . into ( ) , 0 ) ,
1545+ root : nil ( ) ,
1546+ } ;
1547+
1548+ let ( filled_tree, _) = empty_tree
1549+ . insert_frontier_nodes ( frontier. clone ( ) , & Retention :: < ( ) > :: Ephemeral )
1550+ . unwrap ( ) ;
1551+
1552+ assert_eq ! ( filled_tree. frontier( ) , Some ( frontier) ) ;
1553+ }
1554+
12821555 proptest ! {
12831556 #[ test]
12841557 fn clear_flags(
0 commit comments