Skip to content

Commit c95cf74

Browse files
feat(types): add Element::ProvableSumTree variant + NotSummed twin extension
Phase 1 of the ProvableSumTree feature — the missing parallel to ProvableCountTree that bakes the per-node sum into the node hash, making aggregate-sum range queries cryptographically verifiable. This commit adds the types-only foundation (no hash divergence yet — Phase 2 will introduce node_hash_with_sum and the new proof Node variants). DISCRIMINANTS - Element::ProvableSumTree at variant index 17 / bincode discriminant 17 (next free after the NotSummed wrapper byte at 16). This will renumber to 19 when PR #657 (CountIndexedTree) lands and reclaims 17/18. - NonCountedProvableSumTree = 0x80 | 17 = 145. - The NonCounted twin range widened from 0x80..=0x8F (4-bit base) to 0x80..=0x9F (5-bit base) — is_non_counted() now checks the top 3 bits (& 0xe0 == 0x80) instead of the top 4. Existing twins 128..=142 stay put. - The NotSummed twin scheme rebases analogously: prefix 0xb0 -> 0xa0, base mask 0x0F -> 0x1F, family range 0xA0..=0xBF. Existing twins move: NotSummedSumTree 180 -> 164 NotSummedBigSumTree 181 -> 165 NotSummedCountSumTree 183 -> 167 NotSummedProvableCountSumTree 186 -> 170 Plus the new NotSummedProvableSumTree = 0xa0 | 17 = 177. Safe because V1 is pre-shipping. is_not_summed() now uses & 0xe0 == 0xa0. NEW APIS - ElementType::ProvableSumTree, ElementType::NonCountedProvableSumTree, ElementType::NotSummedProvableSumTree. - TreeType::ProvableSumTree (discriminant 11, is_sum_bearing = true, allows_sum_item = true, inner_node_type = ProvableSumNode). - NodeType::ProvableSumNode and TreeFeatureType::ProvableSummedMerkNode(i64) with encode tag byte 7 and a parallel zero_sum() helper alongside zero_count(). - Element::new_provable_sum_tree*, empty_provable_sum_tree*, plus helpers (as_provable_sum_tree_value, is_provable_sum_tree). - Element::NotSummed now accepts ProvableSumTree as a sum-tree inner type (constructor, serialize, deserialize). PROOF DISPATCH ProvableSumTree joins the "provable aggregate parent" family alongside ProvableCountTree / ProvableCountSumTree in ElementType::proof_node_type: subtree children use KvValueHashFeatureType and item children use KvCount. PHASE-1 SCOPE BOUNDARIES ProvableSumTree behaves identically to SumTree for storage, aggregation, and hashing in Phase 1. The divergent node_hash_with_sum and the new proof Node variants (KVSum, KVHashSum, etc.) land in Phase 2. TreeFeatureType::ProvableSummedMerkNode maps to AggregateData::Sum at the Element/aggregate level for now; Phase 2 may introduce a dedicated variant once the hash diverges. Workspace cargo test --all-features green (1497+ tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dbd83dc commit c95cf74

22 files changed

Lines changed: 676 additions & 133 deletions

File tree

grovedb-element/src/element/constructor.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,42 @@ impl Element {
290290
Element::ProvableCountSumTree(maybe_root_key, count_value, sum_value, flags)
291291
}
292292

293+
/// Set element to default empty provable sum tree without flags.
294+
///
295+
/// `ProvableSumTree` is the sum analogue of `ProvableCountTree`: it
296+
/// bakes the per-node sum into the node hash so that aggregate-sum
297+
/// range queries can be cryptographically verified.
298+
pub fn empty_provable_sum_tree() -> Self {
299+
Element::new_provable_sum_tree(Default::default())
300+
}
301+
302+
/// Set element to default empty provable sum tree with flags.
303+
pub fn empty_provable_sum_tree_with_flags(flags: Option<ElementFlags>) -> Self {
304+
Element::new_provable_sum_tree_with_flags(Default::default(), flags)
305+
}
306+
307+
/// Set element to a provable sum tree without flags.
308+
pub fn new_provable_sum_tree(maybe_root_key: Option<Vec<u8>>) -> Self {
309+
Element::ProvableSumTree(maybe_root_key, 0, None)
310+
}
311+
312+
/// Set element to a provable sum tree with flags.
313+
pub fn new_provable_sum_tree_with_flags(
314+
maybe_root_key: Option<Vec<u8>>,
315+
flags: Option<ElementFlags>,
316+
) -> Self {
317+
Element::ProvableSumTree(maybe_root_key, 0, flags)
318+
}
319+
320+
/// Set element to a provable sum tree with flags and sum value.
321+
pub fn new_provable_sum_tree_with_flags_and_sum_value(
322+
maybe_root_key: Option<Vec<u8>>,
323+
sum_value: SumValue,
324+
flags: Option<ElementFlags>,
325+
) -> Self {
326+
Element::ProvableSumTree(maybe_root_key, sum_value, flags)
327+
}
328+
293329
/// Set element to an empty commitment tree.
294330
///
295331
/// Returns `InvalidInput` if `chunk_power > 31`.
@@ -422,19 +458,21 @@ impl Element {
422458
/// parent sum tree's running sum when inserted. Counts (if any) still
423459
/// propagate.
424460
///
425-
/// Only the four sum-tree variants are accepted: `SumTree`, `BigSumTree`,
426-
/// `CountSumTree`, `ProvableCountSumTree`. Any other element — including
427-
/// items, sum items, references, non-sum trees, and any wrapper
428-
/// (`NonCounted`, `NotSummed`) — is rejected with `InvalidInput`.
461+
/// Only the five sum-tree variants are accepted: `SumTree`, `BigSumTree`,
462+
/// `CountSumTree`, `ProvableCountSumTree`, `ProvableSumTree`. Any other
463+
/// element — including items, sum items, references, non-sum trees, and
464+
/// any wrapper (`NonCounted`, `NotSummed`) — is rejected with
465+
/// `InvalidInput`.
429466
pub fn new_not_summed(inner: Element) -> Result<Self, ElementError> {
430467
match inner {
431468
Element::SumTree(..)
432469
| Element::BigSumTree(..)
433470
| Element::CountSumTree(..)
434-
| Element::ProvableCountSumTree(..) => Ok(Element::NotSummed(Box::new(inner))),
471+
| Element::ProvableCountSumTree(..)
472+
| Element::ProvableSumTree(..) => Ok(Element::NotSummed(Box::new(inner))),
435473
_ => Err(ElementError::InvalidInput(
436474
"NotSummed inner element must be a sum-tree variant (SumTree, BigSumTree, \
437-
CountSumTree, or ProvableCountSumTree)",
475+
CountSumTree, ProvableCountSumTree, or ProvableSumTree)",
438476
)),
439477
}
440478
}

grovedb-element/src/element/helpers.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ impl Element {
7171
| Element::ItemWithSumItem(_, sum_value, _)
7272
| Element::SumTree(_, sum_value, _)
7373
| Element::CountSumTree(_, _, sum_value, _)
74-
| Element::ProvableCountSumTree(_, _, sum_value, _) => *sum_value,
74+
| Element::ProvableCountSumTree(_, _, sum_value, _)
75+
| Element::ProvableSumTree(_, sum_value, _) => *sum_value,
7576
_ => 0,
7677
}
7778
}
@@ -107,7 +108,8 @@ impl Element {
107108
Element::NotSummed(inner) => (inner.count_value_or_default(), 0),
108109
Element::SumItem(sum_value, _)
109110
| Element::ItemWithSumItem(_, sum_value, _)
110-
| Element::SumTree(_, sum_value, _) => (1, *sum_value),
111+
| Element::SumTree(_, sum_value, _)
112+
| Element::ProvableSumTree(_, sum_value, _) => (1, *sum_value),
111113
Element::CountTree(_, count_value, _) => (*count_value, 0),
112114
Element::CountSumTree(_, count_value, sum_value, _)
113115
| Element::ProvableCountSumTree(_, count_value, sum_value, _) => {
@@ -129,7 +131,8 @@ impl Element {
129131
| Element::ItemWithSumItem(_, sum_value, _)
130132
| Element::SumTree(_, sum_value, _)
131133
| Element::CountSumTree(_, _, sum_value, _)
132-
| Element::ProvableCountSumTree(_, _, sum_value, _) => *sum_value as i128,
134+
| Element::ProvableCountSumTree(_, _, sum_value, _)
135+
| Element::ProvableSumTree(_, sum_value, _) => *sum_value as i128,
133136
Element::BigSumTree(_, sum_value, _) => *sum_value,
134137
_ => 0,
135138
}
@@ -212,6 +215,31 @@ impl Element {
212215
matches!(self.underlying(), Element::BigSumTree(..))
213216
}
214217

218+
/// Check if the element is a provable sum tree. Looks through wrappers.
219+
pub fn is_provable_sum_tree(&self) -> bool {
220+
matches!(self.underlying(), Element::ProvableSumTree(..))
221+
}
222+
223+
/// Decoded sum value from a `ProvableSumTree`. Looks through wrappers.
224+
pub fn as_provable_sum_tree_value(&self) -> Result<i64, ElementError> {
225+
match self.underlying() {
226+
Element::ProvableSumTree(_, value, _) => Ok(*value),
227+
_ => Err(ElementError::WrongElementType(
228+
"expected a provable sum tree",
229+
)),
230+
}
231+
}
232+
233+
/// Owned variant of [`as_provable_sum_tree_value`].
234+
pub fn into_provable_sum_tree_value(self) -> Result<i64, ElementError> {
235+
match self.into_underlying() {
236+
Element::ProvableSumTree(_, value, _) => Ok(value),
237+
_ => Err(ElementError::WrongElementType(
238+
"expected a provable sum tree",
239+
)),
240+
}
241+
}
242+
215243
/// Check if the element is a tree but not a sum tree. Looks through
216244
/// `NonCounted`.
217245
pub fn is_basic_tree(&self) -> bool {
@@ -229,6 +257,7 @@ impl Element {
229257
| Element::CountSumTree(..)
230258
| Element::ProvableCountTree(..)
231259
| Element::ProvableCountSumTree(..)
260+
| Element::ProvableSumTree(..)
232261
| Element::CommitmentTree(..)
233262
| Element::MmrTree(..)
234263
| Element::BulkAppendTree(..)
@@ -307,6 +336,7 @@ impl Element {
307336
| Element::CountSumTree(Some(_), ..)
308337
| Element::ProvableCountTree(Some(_), ..)
309338
| Element::ProvableCountSumTree(Some(_), ..)
339+
| Element::ProvableSumTree(Some(_), ..)
310340
| Element::CommitmentTree(..)
311341
| Element::MmrTree(..)
312342
| Element::BulkAppendTree(..)
@@ -331,6 +361,7 @@ impl Element {
331361
| Element::CountSumTree(Some(_), ..)
332362
| Element::ProvableCountTree(Some(_), ..)
333363
| Element::ProvableCountSumTree(Some(_), ..)
364+
| Element::ProvableSumTree(Some(_), ..)
334365
)
335366
}
336367

@@ -389,6 +420,7 @@ impl Element {
389420
| Element::CountSumTree(.., flags)
390421
| Element::ProvableCountTree(.., flags)
391422
| Element::ProvableCountSumTree(.., flags)
423+
| Element::ProvableSumTree(.., flags)
392424
| Element::ItemWithSumItem(.., flags)
393425
| Element::CommitmentTree(.., flags)
394426
| Element::MmrTree(.., flags)
@@ -412,6 +444,7 @@ impl Element {
412444
| Element::CountSumTree(.., flags)
413445
| Element::ProvableCountTree(.., flags)
414446
| Element::ProvableCountSumTree(.., flags)
447+
| Element::ProvableSumTree(.., flags)
415448
| Element::ItemWithSumItem(.., flags)
416449
| Element::CommitmentTree(.., flags)
417450
| Element::MmrTree(.., flags)
@@ -435,6 +468,7 @@ impl Element {
435468
| Element::CountSumTree(.., flags)
436469
| Element::ProvableCountTree(.., flags)
437470
| Element::ProvableCountSumTree(.., flags)
471+
| Element::ProvableSumTree(.., flags)
438472
| Element::ItemWithSumItem(.., flags)
439473
| Element::CommitmentTree(.., flags)
440474
| Element::MmrTree(.., flags)
@@ -458,6 +492,7 @@ impl Element {
458492
| Element::CountSumTree(.., flags)
459493
| Element::ProvableCountTree(.., flags)
460494
| Element::ProvableCountSumTree(.., flags)
495+
| Element::ProvableSumTree(.., flags)
461496
| Element::ItemWithSumItem(.., flags)
462497
| Element::CommitmentTree(.., flags)
463498
| Element::MmrTree(.., flags)

grovedb-element/src/element/mod.rs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,26 @@ pub enum Element {
140140
/// at construction and at deserialization.
141141
NonCounted(Box<Element>),
142142
/// Not-summed wrapper: contains a sum-bearing tree variant (`SumTree`,
143-
/// `BigSumTree`, `CountSumTree`, `ProvableCountSumTree`) and behaves
144-
/// identically to it for storage, hashing, and its own internal sum
145-
/// aggregate, but contributes 0 to its parent sum tree's running sum
146-
/// when inserted. Counts still propagate.
143+
/// `BigSumTree`, `CountSumTree`, `ProvableCountSumTree`,
144+
/// `ProvableSumTree`) and behaves identically to it for storage,
145+
/// hashing, and its own internal sum aggregate, but contributes 0 to
146+
/// its parent sum tree's running sum when inserted. Counts still
147+
/// propagate.
147148
///
148149
/// May only be inserted into sum-bearing trees (`SumTree`, `BigSumTree`,
149-
/// `CountSumTree`, `ProvableCountSumTree`).
150+
/// `CountSumTree`, `ProvableCountSumTree`, `ProvableSumTree`).
150151
///
151152
/// Invariants (enforced at construction, serialization, and
152153
/// deserialization):
153-
/// - The inner element MUST be one of the four sum-tree variants above.
154+
/// - The inner element MUST be one of the five sum-tree variants above.
154155
/// - A `NotSummed` may not wrap another `NotSummed`, a `NonCounted`, or
155156
/// any non-tree element.
156157
NotSummed(Box<Element>),
158+
/// Same as Element::SumTree but includes the per-node sum in the
159+
/// cryptographic state. This mirrors `ProvableCountTree` but for sums,
160+
/// allowing aggregate-sum range queries to be cryptographically verified
161+
/// by including the sum in each node hash.
162+
ProvableSumTree(Option<Vec<u8>>, SumValue, Option<ElementFlags>),
157163
}
158164

159165
pub fn hex_to_ascii(hex_value: &[u8]) -> String {
@@ -345,6 +351,17 @@ impl fmt::Display for Element {
345351
Element::NotSummed(inner) => {
346352
write!(f, "NotSummed({})", inner)
347353
}
354+
Element::ProvableSumTree(root_key, sum_value, flags) => {
355+
write!(
356+
f,
357+
"ProvableSumTree({}, {}{})",
358+
root_key.as_ref().map_or("None".to_string(), hex::encode),
359+
sum_value,
360+
flags
361+
.as_ref()
362+
.map_or(String::new(), |f| format!(", flags: {:?}", f))
363+
)
364+
}
348365
}
349366
}
350367
}
@@ -373,6 +390,7 @@ impl Element {
373390
Element::MmrTree(..) => ElementType::MmrTree,
374391
Element::BulkAppendTree(..) => ElementType::BulkAppendTree,
375392
Element::DenseAppendOnlyFixedSizeTree(..) => ElementType::DenseAppendOnlyFixedSizeTree,
393+
Element::ProvableSumTree(..) => ElementType::ProvableSumTree,
376394
Element::NonCounted(inner) => match inner.element_type() {
377395
ElementType::Item => ElementType::NonCountedItem,
378396
ElementType::Reference => ElementType::NonCountedReference,
@@ -391,6 +409,7 @@ impl Element {
391409
ElementType::DenseAppendOnlyFixedSizeTree => {
392410
ElementType::NonCountedDenseAppendOnlyFixedSizeTree
393411
}
412+
ElementType::ProvableSumTree => ElementType::NonCountedProvableSumTree,
394413
// Inner is always a base type — nested wrappers are
395414
// forbidden at construction and (de)serialization.
396415
already_non_counted => already_non_counted,
@@ -400,7 +419,8 @@ impl Element {
400419
ElementType::BigSumTree => ElementType::NotSummedBigSumTree,
401420
ElementType::CountSumTree => ElementType::NotSummedCountSumTree,
402421
ElementType::ProvableCountSumTree => ElementType::NotSummedProvableCountSumTree,
403-
// Inner is always one of the 4 sum-tree variants above —
422+
ElementType::ProvableSumTree => ElementType::NotSummedProvableSumTree,
423+
// Inner is always one of the five sum-tree variants above —
404424
// construction and (de)serialization forbid anything else.
405425
// Returning the inner type is the safest fallback for the
406426
// unreachable case.
@@ -437,11 +457,12 @@ impl Element {
437457
Element::SumTree(..)
438458
| Element::BigSumTree(..)
439459
| Element::CountSumTree(..)
440-
| Element::ProvableCountSumTree(..) => {}
460+
| Element::ProvableCountSumTree(..)
461+
| Element::ProvableSumTree(..) => {}
441462
_ => {
442463
return Err(crate::error::ElementError::InvalidInput(
443464
"NotSummed inner element must be a sum-tree variant (SumTree, \
444-
BigSumTree, CountSumTree, or ProvableCountSumTree)",
465+
BigSumTree, CountSumTree, ProvableCountSumTree, or ProvableSumTree)",
445466
));
446467
}
447468
},
@@ -514,6 +535,7 @@ mod serde_impl {
514535
DenseAppendOnlyFixedSizeTree(u16, u8, Option<ElementFlags>),
515536
NonCounted(Box<ElementShadow>),
516537
NotSummed(Box<ElementShadow>),
538+
ProvableSumTree(Option<Vec<u8>>, SumValue, Option<ElementFlags>),
517539
}
518540

519541
impl From<ElementShadow> for Element {
@@ -544,6 +566,7 @@ mod serde_impl {
544566
ElementShadow::NotSummed(inner) => {
545567
Element::NotSummed(Box::new(Element::from(*inner)))
546568
}
569+
ElementShadow::ProvableSumTree(k, s, f) => Element::ProvableSumTree(k, s, f),
547570
}
548571
}
549572
}
@@ -702,8 +725,8 @@ mod tests {
702725

703726
#[test]
704727
fn element_type_resolves_not_summed_twins() {
705-
// The four sum-tree variants each map to their NotSummed twin.
706-
let cases: [(Element, ElementType); 4] = [
728+
// The five sum-tree variants each map to their NotSummed twin.
729+
let cases: [(Element, ElementType); 5] = [
707730
(
708731
Element::NotSummed(Box::new(Element::SumTree(None, 0, None))),
709732
ElementType::NotSummedSumTree,
@@ -720,6 +743,10 @@ mod tests {
720743
Element::NotSummed(Box::new(Element::ProvableCountSumTree(None, 0, 0, None))),
721744
ElementType::NotSummedProvableCountSumTree,
722745
),
746+
(
747+
Element::NotSummed(Box::new(Element::ProvableSumTree(None, 0, None))),
748+
ElementType::NotSummedProvableSumTree,
749+
),
723750
];
724751
for (element, expected) in cases {
725752
assert_eq!(element.element_type(), expected);

grovedb-element/src/element/serialize.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ impl Element {
3939
Element::SumTree(..)
4040
| Element::BigSumTree(..)
4141
| Element::CountSumTree(..)
42-
| Element::ProvableCountSumTree(..) => {}
42+
| Element::ProvableCountSumTree(..)
43+
| Element::ProvableSumTree(..) => {}
4344
_ => {
4445
return Err(ElementError::CorruptedData(
4546
"NotSummed inner must be a sum-tree variant".to_string(),
@@ -116,7 +117,8 @@ impl Element {
116117
Element::SumTree(..)
117118
| Element::BigSumTree(..)
118119
| Element::CountSumTree(..)
119-
| Element::ProvableCountSumTree(..) => {}
120+
| Element::ProvableCountSumTree(..)
121+
| Element::ProvableSumTree(..) => {}
120122
_ => {
121123
return Err(ElementError::CorruptedData(
122124
"deserialized NotSummed with non-sum-tree inner".to_string(),

grovedb-element/src/element/visualize.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ impl Visualize for Element {
186186
drawer = inner.visualize(drawer)?;
187187
drawer.write(b")")?;
188188
}
189+
Element::ProvableSumTree(root_key, value, flags) => {
190+
drawer.write(b"provable_sum_tree: ")?;
191+
drawer = root_key.as_deref().visualize(drawer)?;
192+
drawer.write(format!(" {value}").as_bytes())?;
193+
194+
if let Some(f) = flags
195+
&& !f.is_empty()
196+
{
197+
drawer = f.visualize(drawer)?;
198+
}
199+
}
189200
}
190201
Ok(drawer)
191202
}

0 commit comments

Comments
 (0)