Skip to content

Commit a85f224

Browse files
nuttycomclaude
andcommitted
Implement LocatedPrunableTree::frontier() to extract a NonEmptyFrontier
Traverses the rightmost path of the tree, collecting the frontier leaf hash and ommer hashes (left sibling root hashes at each right turn). Returns None when the tree is empty, has incomplete left siblings, or has pruned subtrees on the rightmost path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8bab449 commit a85f224

1 file changed

Lines changed: 274 additions & 1 deletion

File tree

shardtree/src/prunable.rs

Lines changed: 274 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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>(
10511100
mod 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

Comments
 (0)