Skip to content

feat: add Element::NotSummed wrapper to opt-out of sum propagation#659

Merged
QuantumExplorer merged 7 commits into
developfrom
feat/not-summed-foundation
May 10, 2026
Merged

feat: add Element::NotSummed wrapper to opt-out of sum propagation#659
QuantumExplorer merged 7 commits into
developfrom
feat/not-summed-foundation

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Symmetric counterpart to Element::NonCounted (#654). Where NonCounted opts a child out of count propagation in count-bearing trees, NotSummed opts a sum-bearing subtree out of sum propagation in sum-bearing trees.

Use case: a SumTree of category subtotals, where one inner sum-tree should be informational only — it tracks its own sum but does not bubble up to the parent total.

What was done?

Added a new Element::NotSummed(Box<Element>) variant that:

  • behaves identically to its inner element for storage, hashing, and the inner sum-tree's own internal sum aggregate,
  • contributes 0 to the parent sum tree's running sum,
  • still contributes the inner element's count to a parent count tree (only sum is suppressed),
  • may only wrap one of the four sum-tree variants (SumTree, BigSumTree, CountSumTree, ProvableCountSumTree),
  • may only be inserted into a sum-bearing parent (SumTree, BigSumTree, CountSumTree, ProvableCountSumTree).

Design choice: stricter inner-type whitelist than NonCounted

NonCounted can wrap any element (including items, references, all tree types). NotSummed restricts the inner to the four sum-tree variants because that is where the wrapper is semantically meaningful — a sum-bearing subtree that retains its own internal sum but contributes nothing to the parent. Wrapping items or non-sum trees would have no effect distinct from using the bare type.

The constructor Element::new_not_summed returns Result and rejects everything else with InvalidInput. Serialization and deserialization enforce the same whitelist, plus an O(1) two-byte pre-check that rejects all four wrapper-on-wrapper combinations ([15,15], [15,16], [16,15], [16,16]) before bincode can recurse — closing a stack-exhaustion vector.

Discriminant scheme

bincode (Element) ElementType flag bit twins
NonCounted (existing) 15 0x80 128..=142 (15 twins)
NotSummed (new) 16 0x40 68, 69, 71, 74 (only 4 twins)

The disjoint flag bits keep is_non_counted() and is_not_summed() independently testable on ElementType. base() strips whichever flag is set. The two wrappers are mutually exclusive in practice — constructors and (de)serializers reject any nesting, including cross-nesting NotSummed(NonCounted(_)) and NonCounted(NotSummed(_)).

Cryptographic notes

The wrapper byte is part of the serialized value, so the value hash distinguishes NotSummed(X) from a bare X. The parent's TreeFeatureType for a not-summed child is the inner element's feature type with its sum component zeroed (via a new TreeFeatureType::zero_sum() helper, symmetric to zero_count()); no new TreeFeatureType variants are introduced.

Scope

Touched 27 files across grovedb-element, grovedb-query, grovedb-merk, and grovedb. Mirrors PR #654's wiring throughout the stack:

  • enum + discriminant + constructor + helpers + serialization
  • merk: TreeType::is_sum_bearing(), insert validation, feature-type dispatch, get/cost paths, reconstruct preserves wrapper
  • grovedb: batch propagation tracks not_summed flag alongside non_counted in InsertTreeWithRootHash, parent-type guard, estimated cost overhead, debugger transparency
  • existing tests in tests/{batch_unit,batch_coverage,batch_rejection}_tests.rs updated for the new field

How Has This Been Tested?

New tests:

  • grovedb-element (10 tests): constructor whitelist (rejects items, sum items, plain trees, count trees, all wrapper nesting; accepts the four sum-tree variants); helper passthrough (is_*, sum_value_or_default → 0, count_value_or_default still propagates, count_sum_value_or_default → (count, 0), flag accessors); bincode round-trip; reject nested wrappers at deserialize and at serialize; long-chain pre-check stack-overflow guard.
  • grovedb-element discriminant pinning: pins NOT_SUMMED_WRAPPER_DISCRIMINANT = 16 and the four 0x40 | base twin discriminants; rejects all illegal inner bytes including the wrapper bytes themselves and the synthetic twin range.
  • grovedb-merk (5 tests): insert into NormalTree and CountTree is rejected; insert into SumTree succeeds and contributes 0; NotSummed(SumTree(_,100,_)) in a ProvableCountSumTree yields count=1 sum=0; reconstruct_with_root_key preserves the wrapper through propagation; constructor rejects non-sum-tree inner.
  • grovedb (tests/not_summed_tests.rs, 5 tests): batch insert rejected into NormalTree and CountTree; direct insert in SumTree excludes subtree sum from parent aggregate; batch propagation preserves wrapper through InsertTreeWithRootHash (with both wrapper insert AND child insert in the same batch); check_subtree_exists works through the wrapper.

Full workspace test suite passes:

  • cargo test -p grovedb-element — 74 passed (57 unit + 17 integration)
  • cargo test -p grovedb-query — 235 passed
  • cargo test -p grovedb-merk --features minimal,test_utils,full — 416 passed
  • cargo test -p grovedb — 1495 passed

Breaking Changes

None for existing callers. The change adds a new Element variant and is additive at the API surface. The on-disk serialization format gains bincode discriminant 16 — older code reading data written with this variant would fail to deserialize, so producer/consumer pairs need to be upgraded together. This matches the existing "ONLY APPEND TO THIS LIST" comment on the Element enum.

The internal GroveOp::InsertTreeWithRootHash variant gains a not_summed: bool field; the variant is #[non_exhaustive] and only constructed inside this crate, but the four in-tree test sites that build it directly are updated.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

🤖 Generated with Claude Code

Symmetric counterpart to Element::NonCounted (#654). Where NonCounted
opts a child out of count propagation in count-bearing trees, NotSummed
opts a sum-bearing subtree out of sum propagation in sum-bearing trees.

The wrapper is strictly typed: it can ONLY contain one of the four
sum-tree variants (SumTree, BigSumTree, CountSumTree,
ProvableCountSumTree), and may only be inserted into sum-bearing parent
trees. Items, sum items, references, non-sum trees, and any wrapper
nesting are rejected at construction, serialization, and
deserialization.

When a NotSummed-wrapped sum-tree is inserted into a sum-bearing parent,
it contributes 0 to the parent's running sum. Counts (if any) still
propagate. The inner sum-tree retains its own internal sum aggregate
unchanged — only the outward propagation is suppressed.

Discriminant scheme:
- bincode 16 (Element variant)
- ElementType twins at 0x40 | base: 68, 69, 71, 74 (only the four legal
  sum-tree base discriminants). The 0x40 flag bit is disjoint from
  NonCounted's 0x80, so wrapper status is independently testable.

Stack-overflow defense: deserialize pre-checks the leading two bytes
for any wrapper combination ([15,15], [15,16], [16,15], [16,16]) and
rejects before bincode recurses through the Box<Element> chain.

Wiring:
- grovedb-element: enum + constructor whitelist + serialize/deserialize
  guards + helpers (sum_value_or_default → 0, count still propagates) +
  ElementType twins + visualize.
- grovedb-query: TreeFeatureType::zero_sum() symmetric helper.
- merk: TreeType::is_sum_bearing(), insert/insert_subtree/
  insert_reference parent-type guards, reconstruct preserves wrapper,
  costs/get account for the wrapper byte.
- grovedb: batch propagation tracks not_summed alongside non_counted in
  InsertTreeWithRootHash, parent-type guard, estimated cost overhead,
  debugger renders inner element transparently.

Tests (725+ existing pass, plus new):
- grovedb-element: constructor whitelist, helper passthrough, bincode
  round-trip, deserialize rejects nested chains and non-sum-tree inner.
- grovedb-merk: insert rejected in NormalTree/CountTree, accepted in
  SumTree contributes 0, NotSummed(SumTree(_,100,_)) in
  ProvableCountSumTree → count=1 sum=0, reconstruct preserves wrapper.
- grovedb: 5 end-to-end tests covering batch parent-type guard,
  batch propagation preserving the wrapper, check_subtree_exists
  through the wrapper, and outer sum aggregate excluding the subtree.

Breaking changes: none for existing callers. The on-disk serialization
format gains bincode discriminant 16 — older code reading data written
with this variant fails to deserialize, matching the existing
"ONLY APPEND TO THIS LIST" comment on Element.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Review Change Stack

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 46 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 372951dd-2277-41df-9f96-dbb420609c29

📥 Commits

Reviewing files that changed from the base of the PR and between 1206049 and b00b830.

📒 Files selected for processing (29)
  • grovedb-element/Cargo.toml
  • grovedb-element/src/element/constructor.rs
  • grovedb-element/src/element/helpers.rs
  • grovedb-element/src/element/mod.rs
  • grovedb-element/src/element/serialize.rs
  • grovedb-element/src/element/visualize.rs
  • grovedb-element/src/element_type.rs
  • grovedb-query/src/proofs/tree_feature_type.rs
  • grovedb/src/batch/mod.rs
  • grovedb/src/debugger.rs
  • grovedb/src/estimated_costs/average_case_costs.rs
  • grovedb/src/estimated_costs/worst_case_costs.rs
  • grovedb/src/lib.rs
  • grovedb/src/operations/get/query.rs
  • grovedb/src/operations/insert/mod.rs
  • grovedb/src/operations/proof/generate.rs
  • grovedb/src/operations/proof/verify.rs
  • grovedb/src/reference_path.rs
  • grovedb/src/tests/batch_coverage_tests.rs
  • grovedb/src/tests/batch_rejection_tests.rs
  • grovedb/src/tests/batch_unit_tests.rs
  • grovedb/src/tests/mod.rs
  • grovedb/src/tests/not_summed_tests.rs
  • merk/src/element/costs.rs
  • merk/src/element/get.rs
  • merk/src/element/insert.rs
  • merk/src/element/reconstruct.rs
  • merk/src/element/tree_type.rs
  • merk/src/tree_type/mod.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/not-summed-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@QuantumExplorer
Copy link
Copy Markdown
Member Author

@coderabbitai review

This is Claude. The PR is intentionally in draft while we wait for the first review pass — please review it now.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

@QuantumExplorer Sure, I'll start the review of PR #659 now!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

❌ Patch coverage is 91.83938% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.74%. Comparing base (1206049) to head (b00b830).

Files with missing lines Patch % Lines
grovedb/src/batch/mod.rs 51.16% 21 Missing ⚠️
grovedb-element/src/element/mod.rs 93.41% 11 Missing ⚠️
grovedb-element/src/element/constructor.rs 83.33% 4 Missing ⚠️
grovedb-element/src/element/helpers.rs 97.03% 4 Missing ⚠️
grovedb/src/operations/get/query.rs 0.00% 4 Missing ⚠️
merk/src/element/insert.rs 95.00% 4 Missing ⚠️
merk/src/element/get.rs 70.00% 3 Missing ⚠️
grovedb/src/estimated_costs/worst_case_costs.rs 66.66% 2 Missing ⚠️
grovedb/src/operations/proof/generate.rs 0.00% 2 Missing ⚠️
grovedb/src/operations/proof/verify.rs 0.00% 2 Missing ⚠️
... and 6 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #659      +/-   ##
===========================================
+ Coverage    90.64%   90.74%   +0.10%     
===========================================
  Files          184      184              
  Lines        54826    55532     +706     
===========================================
+ Hits         49699    50395     +696     
- Misses        5127     5137      +10     
Components Coverage Δ
grovedb-core 88.53% <46.03%> (-0.06%) ⬇️
merk 92.26% <95.18%> (+0.17%) ⬆️
storage 86.36% <ø> (ø)
commitment-tree 96.43% <ø> (ø)
mmr 96.76% <ø> (ø)
bulk-append-tree 89.14% <ø> (ø)
element 95.75% <95.91%> (+0.51%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@QuantumExplorer QuantumExplorer marked this pull request as ready for review May 10, 2026 20:51
…tree extensions

CodeRabbit's Codecov pass on PR #659 flagged 86.42% patch coverage. Most
gaps were unreachable!() arms (genuinely uncoverable), but several
testable additions had no direct unit tests yet.

- grovedb-query/src/proofs/tree_feature_type.rs: new tests module
  covering both new and existing helpers — `zero_sum` (each variant +
  no-op cases), `zero_count` (mirror), `count`. The file had no test
  module previously.
- grovedb-element/src/element/mod.rs: new tests for `Display` of
  `NotSummed` and `NonCounted`, plus `element_type()` resolving to the
  correct `NotSummedXxx` / `NonCountedXxx` synthetic twin for each
  legal inner.
- merk/src/element/tree_type.rs: tests asserting all
  `ElementTreeTypeExtensions` methods (`tree_type`, `maybe_tree_type`,
  `root_key_and_tree_type{,_owned}`, `tree_flags_and_type`,
  `tree_feature_type`) delegate through the `NotSummed` wrapper, plus
  `get_feature_type` zeros sum (and propagates count where
  applicable) for all four sum-bearing parent tree types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review findings:

  1. [P1] Reject cross-wrapper construction (grovedb-element/src/element/constructor.rs:393-407)

new_non_counted only rejects NonCounted, so it can still construct NonCounted(NotSummed(...)) even though serialization rejects cross-wrapper nesting. That value can reach batch execution with is_non_counted() == true and underlying() == NotSummed, then hit the supposedly-unreachable wrapper arm instead of returning a typed error.

Suggested fix: make new_non_counted reject both wrapper variants, and make into_non_counted avoid wrapping NotSummed as well, either by making it fallible or by adding a separate checked helper for external callers. Add a batch regression for NonCounted(NotSummed(SumTree)) to confirm it returns an InvalidInput / InvalidBatchOperation instead of panicking.

  1. [P2] Add serde-side wrapper validation (grovedb-element/src/element/mod.rs:45-47)

The bincode serialize/deserialize paths now reject nested wrappers and NotSummed with a non-sum-tree inner, but the serde feature still derives recursive Deserialize for Element. With serde enabled, a payload can construct invalid NotSummed(Item) / cross-wrapper values, and deeply nested wrapper payloads still recurse before any validation runs.

Suggested fix: replace the derived serde deserialize path with a manual visitor or serde helper wrappers that enforce the same invariants as Element::deserialize: wrapper inners must not be wrappers, and NotSummed may only wrap SumTree, BigSumTree, CountSumTree, or ProvableCountSumTree. Add serde-feature tests for nested wrapper rejection and NotSummed(non_sum_tree) rejection.

QuantumExplorer and others added 5 commits May 11, 2026 04:09
…hains

`reference_path::follow_reference` matched on the resolved `element`
directly, so a `NonCounted(Reference)` would have been returned as a
target value rather than followed as a hop. The function is currently
only used from tests (production reference resolution goes through
`GroveDb::follow_reference` in `operations/get/mod.rs`, which already
unwraps via `into_underlying()`), but the divergence was a latent
trap for any future wiring.

Symmetric to the existing get-path unwrap. The wrapper byte is part of
`value_hash` (computed from the on-disk bytes earlier in the function),
so dropping it from `target_element` does not affect cryptographic
verification of subsequent hops.

`NotSummed` cannot wrap a reference (constructor whitelist enforces
sum-tree variants), so this fix is purely forward-safe and symmetric
to the `NonCounted` handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Codex's review of PR #659.

**P1 — Reject cross-wrapper construction**
`new_non_counted` previously rejected only `NonCounted` inners, so a
caller could build `NonCounted(NotSummed(SumTree))` even though
serialization rejects cross-wrapper nesting. Such a value would reach
batch execution with `is_non_counted() == true` and
`underlying() == NotSummed`, then hit the supposedly-unreachable
wrapper arm.

- `Element::new_non_counted` now rejects both `NonCounted` and
  `NotSummed` inners with `InvalidInput`.
- `Element::into_non_counted` is now fallible (`Result<Self,
  ElementError>`) and returns `Err` for `NotSummed` input. Two
  in-tree callers in `grovedb/src/batch/mod.rs` already pass
  freshly-constructed bare tree elements; updated to `.expect(..)`
  with a comment documenting the precondition.
- New regression in `grovedb/src/tests/not_summed_tests.rs` builds a
  hand-rolled `NonCounted(NotSummed(SumTree))` (bypassing the
  constructor) and asserts `apply_batch` rejects it as a typed error
  rather than panicking.
- Two new unit tests in `grovedb-element/src/element/helpers.rs`:
  `into_non_counted_rejects_not_summed` and
  `new_non_counted_rejects_not_summed`.

**P2 — Manual serde Deserialize with wrapper validation**
The previous `derive(serde::Deserialize)` did not enforce the wrapper
invariants. With the `serde` feature enabled, payloads could
construct invalid `NotSummed(Item)` or cross-wrapper values, and
deeply-nested wrapper payloads recursed before any check fired.

- Replaced the derive on `Element` with a manual `Deserialize` impl
  in a `serde_impl` module, gated behind `feature = "serde"`.
- The impl deserializes through a private `ElementShadow` mirror enum
  (`#[serde(rename = "Element")]` so the wire format is unchanged),
  converts to `Element`, then calls
  `Element::check_recursive_wrapper_invariants` to validate every
  level of the tree.
- New public `Element::validate_wrapper_invariants` codifies the
  rules in one place (used by both the serde path and available to
  external callers).
- `Serialize` derive is preserved — serialization of a valid Element
  is always safe.

New serde-feature tests in `grovedb-element/src/element/mod.rs`:
- `serde_round_trip_valid_elements` — JSON round-trip for Item,
  SumTree, NonCounted(Item), NotSummed(SumTree).
- `serde_rejects_nested_non_counted` / `serde_rejects_nested_not_summed`
  — nested same-wrapper payloads.
- `serde_rejects_cross_wrapper_nesting` — both
  `NonCounted(NotSummed(_))` and `NotSummed(NonCounted(_))`.
- `serde_rejects_not_summed_with_non_sum_tree_inner` — six illegal
  inner types covering items, plain trees, count trees, MMR.
- `serde_rejects_deeply_nested_wrapper_chain` — depth-64 chain
  rejected via the per-level check fired during recursive
  conversion.

Adds `serde_json` as a dev-dependency for the JSON round-trip tests.

Test counts: grovedb-element 63 lib (was 57 + 6 serde = 63),
merk 418, grovedb 1496 (was 1495 + 1 regression).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from review:

**1. Replace `into_not_summed_unchecked` (panicking) with fallible
`into_not_summed`.** The panic path was a latent foot-gun. The new
helper mirrors `into_non_counted`'s API: returns `Result`, idempotent
on `NotSummed`, returns `Err(InvalidInput)` for `NonCounted` (mutually
exclusive) or any non-sum-tree variant. The single in-tree caller
(batch propagation) now uses `.expect(..)` with a comment documenting
that the input is always a freshly-constructed bare sum-tree element.

**2. Move `NotSummed` twin discriminants from `0x40 | base` to
`0xb0 | base`.** This places the four twins at 180, 181, 183, 186 —
inside the high-bit-set range alongside `NonCounted` twins (128..142),
rather than down at 64..78 mid-range. The two ranges are still
distinguished by the upper nibble: `0x80` for `NonCounted`, `0xb0` for
`NotSummed`. Detection is now an upper-nibble compare instead of a
single-bit test:
- `is_non_counted`: `disc & 0xf0 == 0x80` (was `disc & 0x80 != 0`).
- `is_not_summed`: `disc & 0xf0 == 0xb0` (was `disc & 0x40 != 0`).

The single-bit `is_non_counted` would have started returning true for
`NotSummed` twins too once they shared bit 7, which would have been
wrong. Upper-nibble compares keep the predicates disjoint. Renamed
the now-misnamed `NOT_SUMMED_FLAG` constant to `NOT_SUMMED_TWIN_PREFIX`
(`= 0xb0`) and updated `NOT_SUMMED_BASE_MASK` to `0x0F` (sufficient
since base discriminants are 0..14).

Updated the `try_from`, `from_serialized_value`, the discriminant-pinning
test, and added a new `test_not_summed_helpers` mirroring
`test_non_counted_helpers`.

**3. Remove unused `TreeFeatureType::zero_sum` helper.** It was added
preemptively for symmetry with `zero_count` but had no production
callers — `Element::sum_value_or_default` already returns 0 for
`NotSummed`, so `get_feature_type` produces the right zero-summed
feature type without needing this helper. Removed the function and
its unit test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `serde_impl` module already had a comment describing the *approach*
(shadow + From conversion + recursive validation), but not the *why* —
which serde patterns fail and which alternatives exist. Codifies the
trade-off so future maintainers can see at a glance that:

- `#[serde(try_from)]` needs a separate source type (no self-pointing).
- `#[serde(remote)]` is for foreign types only.
- `#[serde(deserialize_with)]` is field-level.
- `#[serde(transparent)]` / `flatten` are for single-field structs.

Leaving three real options for derive-and-validate: shadow enum,
manual Visitor, or drop the derive. We keep the derive (external
tooling consumers may rely on it) and use the shadow as the shortest
correct form.

Doc-only change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rors

The three `.expect(..)` calls added in commit 4a93a88 around the
batch propagation wrapper-rewrap path would panic if a future change
ever passed a pre-existing wrapper element through these branches.
Replace them with `CorruptedCodeExecution` errors so the failure
surfaces as a typed `Result` and bubbles up via the cost-aware return
machinery instead of aborting the process.

Sites:
- `GroveOp::InsertTreeWithRootHash` propagation: `into_non_counted` and
  `into_not_summed` calls now `map_err(..)` to `CorruptedCodeExecution`,
  drained via `cost_return_on_error_no_add!`.
- `GroveOp::InsertNonMerkTree` propagation: same treatment for the
  `into_non_counted` call.

Behavior in the happy path is unchanged — the input elements are
freshly built from `aggregate_data` / `meta.to_element(..)`, never
wrapped, and the wrappers always succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

Reviewed

@QuantumExplorer QuantumExplorer merged commit dbd83dc into develop May 10, 2026
10 of 11 checks passed
@QuantumExplorer QuantumExplorer deleted the feat/not-summed-foundation branch May 10, 2026 21:49
QuantumExplorer added a commit that referenced this pull request May 10, 2026
Two PRs landed on develop while this PR was open:
- #659: Element::NotSummed wrapper variant
- #658: aggregate_count proof verification under verify feature

Conflicts resolved in 8 files. The substantive resolution work:

DISCRIMINANT COLLISION. The shipped cidx PR used ElementType
discriminant 16 for CountIndexedTree and 17 for
ProvableCountIndexedTree. Develop's NotSummed PR allocated byte 16
as NOT_SUMMED_WRAPPER_DISCRIMINANT. Both are still pre-shipping, so
renumbering is safe — and since the NotSummed wrapper byte semantics
need to round-trip across the wire, the wrapper byte stays at 16
and the cidx discriminants shift:

  CountIndexedTree:                 16 → 17
  ProvableCountIndexedTree:         17 → 18
  NonCountedCountIndexedTree:       144 → 145 (= 0x80 | 17)
  NonCountedProvableCountIndexedTree: 145 → 146 (= 0x80 | 18)

ENUM VARIANT ORDER. Bincode encodes Element variants by ENUM ORDER
(not by ElementType discriminant). The Element enum has been
reordered so NotSummed appears at variant index 16 (matching
NOT_SUMMED_WRAPPER_DISCRIMINANT), and CountIndexedTree /
ProvableCountIndexedTree appear AFTER NotSummed at indices 17/18 —
matching their new ElementType discriminants. Without this
reordering, bincode would still write 16 for CountIndexedTree but
from_serialized_value would interpret 16 as the NotSummed wrapper.

WRAPPER NESTING CHECK in `from_serialized_value`: now explicitly
rejects both nested NonCounted and cross-nesting with NotSummed
(inner_byte == 15 || inner_byte == 16), in addition to the
existing reject-high-bit-twin guard.

Other resolutions are straightforward additions: both branches
added arms to match expressions across helpers.rs, tree_type.rs,
visualize.rs; merged additively. The two `mod tests` blocks in
grovedb-element/src/element/mod.rs (one from each PR) renamed the
cidx-side block to `cidx_tests` to avoid duplicate name.

All 1944+ workspace tests pass post-merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request May 11, 2026
[P1] 247-byte ceiling on cidx primary item keys (direct + batch).
Secondary keys are (count_be ‖ item_key); Merk requires keys < 256
bytes. Generic batch validation only enforces 255-byte cap, so
cidx primaries need an 8-byte stricter ceiling. Added
MAX_CIDX_ITEM_KEY_LEN = 247 and validate_cidx_item_key_len. Enforced
on insert_into_count_indexed_tree_on_transaction and on
execute_ops_on_path when in_tree_type is cidx primary.

[P1] insert_into_count_indexed_tree overwrite cleanup. The dedicated
API read existing element only for count delta — didn't clean up
old tree storage when replacing a tree entry. Mirrors batch
safe-subset semantics:
  - existing tree, new non-tree: ALLOW + cleanup
  - existing tree, new empty tree (root_key=None): ALLOW + cleanup
  - existing cidx, new non-empty cidx: REJECT (ambiguous)
  - existing tree, new non-empty non-cidx tree: REJECT (ambiguous)
Cleanup mirrors db.delete/batch DeleteTree: find_subtrees + clear,
plus secondary namespace clear for existing-cidx.

[P2] Batch consistency check for safe-subset overwrite descendants.
When scheduling cidx-overwrite cleanup, scan ops_by_qualified_paths
for descendants of the cidx path and reject as
InvalidBatchOperation if found. Defense in depth; the audit's worry
about silent cleanup-drop is already caught by other checks in
most flavors, but the new check provides a clearer error message.

[P2] verify_grovedb duplicate-secondary detection. Changed
HashMap<Vec<u8>, u64> to HashMap<Vec<u8>, Vec<u64>> so duplicate
secondary rows for the same primary key (real drift class) are
flagged via __cidx_secondary_duplicate__ sentinel path.

[P2] appendix-a.md: updated discriminants 16/17/144/145 → 17/18/
145/146, added NotSummed wrapper byte row, added 247-byte ceiling
note. Now matches code (post-merge with #659).

All 1611 grovedb tests pass; 5 new audit-targeted tests added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant