Skip to content

Commit 9e495e4

Browse files
nuttycomclaude
andcommitted
Implement ShardTree::frontier() to extract a full-depth NonEmptyFrontier
Adds a frontier_ommers() helper to LocatedPrunableTree that returns the raw (position, leaf, ommers) without from_parts validation, enabling ShardTree::frontier() to assemble a complete frontier by combining within-shard ommers with sibling subtree root hashes above the shard level. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a85f224 commit 9e495e4

2 files changed

Lines changed: 153 additions & 4 deletions

File tree

shardtree/src/lib.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

shardtree/src/prunable.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,9 +1028,13 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
10281028
result
10291029
}
10301030

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>> {
1031+
/// Returns the position, leaf hash, and ommers for the frontier of this tree,
1032+
/// or `None` if the frontier cannot be determined.
1033+
///
1034+
/// The returned ommers cover only the levels within this tree (from level 0 up to
1035+
/// but not including this tree's root level). This is a building block for
1036+
/// [`ShardTree::frontier`] which extends the ommers to the full tree depth.
1037+
pub(crate) fn frontier_ommers(&self) -> Option<(Position, H, Vec<H>)> {
10341038
/// Traverses the rightmost path of the tree, collecting the frontier leaf and ommers.
10351039
/// Returns `(position, leaf_hash, ommers)` with ommers ordered from lowest to highest
10361040
/// level, or `None` if the frontier cannot be extracted.
@@ -1073,7 +1077,13 @@ impl<H: Hashable + Clone + PartialEq> LocatedPrunableTree<H> {
10731077
}
10741078
}
10751079

1076-
let (position, leaf, ommers) = go(self.root_addr, &self.root)?;
1080+
go(self.root_addr, &self.root)
1081+
}
1082+
1083+
/// Returns the Merkle frontier of the tree, if the tree is nonempty and has no `Nil` leaves
1084+
/// prior to the leaf at the greatest position.
1085+
pub fn frontier(&self) -> Option<NonEmptyFrontier<H>> {
1086+
let (position, leaf, ommers) = self.frontier_ommers()?;
10771087
NonEmptyFrontier::from_parts(position, leaf, ommers).ok()
10781088
}
10791089
}

0 commit comments

Comments
 (0)