fix(costs): version default-section storage removal accounting - #726
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (8)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThis PR fixes storage-removal arithmetic that dropped default-section entries. It adds version-gated behavior, keeps legacy behavior for V1–V3, enables corrected behavior in V4, and applies the setting across batch, deletion, and Merk operations. ChangesStorage Removal Addition Versioning
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to The change preserves legacy accounting for older versions while correcting default-section removal accounting in V4; no actionable merge-blocking risk remains beyond normal checks and review. Sequence Diagram(s)sequenceDiagram
participant GroveVersion
participant GroveDBOperation
participant Merk
participant RemovalArithmetic
GroveVersion->>GroveDBOperation: provide storage-cost version
GroveDBOperation->>RemovalArithmetic: activate scoped version guard
GroveVersion->>Merk: provide storage-cost version
Merk->>RemovalArithmetic: apply version during walker and commit
RemovalArithmetic-->>GroveDBOperation: calculate versioned removal total
RemovalArithmetic-->>Merk: calculate versioned removal total
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #726 +/- ##
===========================================
+ Coverage 92.56% 92.60% +0.04%
===========================================
Files 307 307
Lines 95705 95756 +51
===========================================
+ Hits 88585 88672 +87
+ Misses 7120 7084 -36
🚀 New features to boost your workflow:
|
877c46b to
0b58b11
Compare
|
This is Claude (AI-assisted). Pushed a rework (commit 5d827ed) addressing the consensus-safety problem in the previous version. The bug being fixed: the default-section-drop only ever affected three of the four basic-into-sectioned arms — The prior version routed all four arms through the version selector, so the legacy/v0 path dropped the default section for the fourth arm too — i.e. it changed shipped v1/v2 output (its own regression test even asserted the wrong Change:
On the thread-local: I looked at threading the version explicitly instead, but the combination flows through Verified: |
* feat(version): add GROVE_V4, behaviourally identical to V3 GROVE_V3 is live, so a fix that changes an accepted/rejected outcome, a committed root hash, or a tracked cost cannot be applied unconditionally — nodes carrying it would diverge from nodes that do not. There is currently nowhere for such a fix to land, which has left several of them stuck: - #776: overwriting an indexed tree with a bare Reference skips the per-axis secondary cleanup. Closing it costs an extra stored-element read on EVERY reference overwrite (+1 seek, +79 storage_loaded_bytes, measured by the refresh-reference cost tests), and references over plain trees are shipped functionality. - Batch DeleteTree treats the caller-declared tree type as authoritative when selecting cleanup namespaces. Reading the stored element instead fixes both an indexed type-confusion and a live CommitmentTree wrong-emptiness-path bug, but adds a read to a released path. - Per project notes, five audit-fix PRs (#726, #730, #732, #734, #739) are gated on v3 and need re-gating before they can merge. This adds the version and nothing else. Every method-version slot is copied from V3 unchanged, so activating protocol version 4 today is a no-op; each gate is a deliberate, separately-reviewable slot bump. Verified rather than assumed: registering V4 changes what `GroveVersion::latest()` resolves to, and the whole test suite defaults to latest. The full workspace suite passes with V4 as latest (2459 grovedb + 705 merk + the rest), and the only two failures were the version registry's own self-describing tests — `grove_version_latest_returns_v3` and `grove_versions_count` — which are updated here. That is the evidence the change is inert. Adds `grove_v4_is_behaviourally_identical_to_v3_until_a_gate_is_added`, which compares every slot and fails the moment one is bumped. That failure is the intended prompt to document the gate rather than let V4 accrete behaviour silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(version): drop the V3/V4 slot-identity assertion It pinned V4's initial state as inert, which was worth verifying once but becomes churn the moment a gate is added — and the DeleteTree read and #776 are both queued to gate on V4 next, so it would fail immediately and be deleted anyway. The evidence it provided is preserved where it belongs: the PR description records that the full workspace suite passed with V4 as latest and that only the registry's own self-describing tests changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0ab0499 to
6fbb628
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
This is Claude (AI-assisted). Assessment of how needed this fix is, and why the PR is being marked deferred rather than merged now. TL;DR: the fix is correct, V4-gated, CI-green and the legacy path is pinned byte-exact — but the bug has no effect on any fee, refund, pool credit, or root hash Platform computes today. It is hygiene / future-proofing, not a live defect, so it is parked rather than carrying a thread-local version selector into What the bug can actually lose
How Drive reaches the buggy arms
What happens when it does fire
Why defer rather than merge
State of the branch (so it can be picked back up quickly)
|
The default-section-drop bug only ever affected three of the four basic-into-sectioned arms (Add: Basic+Sectioned, Add: Sectioned+Basic, AddAssign: Basic+=Sectioned). The fourth, AddAssign: Sectioned+=Basic, reinserted the default section correctly in every shipped version. The prior rework routed all four through the version selector, which made the legacy/v0 path *drop* the default section for the fourth arm too — regressing shipped v1/v2 output (its own test asserted the wrong 0). Route Sectioned+=Basic through the always-correct helper unconditionally and assert the preserved total (13). Document why the thread-local selector exists and which arms it governs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GROVE_V3 went live on mainnet (~mid-June 2026) with the legacy default-section-dropping removal arithmetic, so the issue #683 fix can no longer activate in v3 without forking replay of v3 blocks. Move the activation to GROVE_V4: v1..v3 keep the legacy behavior, v4+ preserves the default section. Adds a v3 regression test asserting the legacy undercount is kept, and extends the version-gate test to cover v4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v3 regression only asserted the legacy total was below the inserted
bytes. Replay compatibility depends on the exact shipped value, so pin it:
under GROVE_V3 the combined Basic+=Sectioned removal is an empty sectioned
map (0 of 155 added bytes), under GROVE_V4 it is {default: {UNKNOWN_EPOCH:
155}}. Both tests share one helper that also captures the key/value bytes
the sectional callback observed, so the undercount is stated explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit group C008 (September 2026 addendum on #683) asked for every mixed Basic/Sectioned removal arm and aggregation order to be pinned under both the legacy (V1..V3) and corrected (V4) arithmetic, with a default owner that already carries epoch attribution so both losses — existing attribution and incoming basic bytes — are visible. - costs: exact-map matrix for `Basic + Sectioned`, `Sectioned + Basic`, `Basic += Sectioned`, `Sectioned += Basic` over three default-section shapes (present with UNKNOWN_EPOCH, present without it, absent), under version 0 and 1; `Sectioned += Basic` asserted identical unguarded, v0 and v1; guard nests/restores and the unguarded default is legacy. - grovedb: two epoch-flagged items deleted in one `apply_batch_with_element_flags_update` batch — V4 yields one default section carrying both epochs plus the key bytes under UNKNOWN_EPOCH (302 of 302 bytes); V3 pinned to the empty legacy map (0 of 302). - docs/crates/costs.md: per-arm behaviour table, the guarded entry points, and the requirement that aggregation outside a GroveDB call install the guard to get the corrected arithmetic. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6fbb628 to
c00356e
Compare
|
This is Claude (AI-assisted). Brought the branch up to date and validated it against the September 2026 audit addendum on #683 (group C008). Draft status and the Rebase. Rebased the four existing commits onto Validation matrix added (the addendum's three items):
Local runs: Why it was deferred (unchanged). The three buggy arms only drop the default-identifier ( |
Summary
Fixes #683. Combining a
BasicStorageRemovalwith aSectionedStorageRemovalcan discard the default identifier's entire epoch map, losing both its existing bytes and the basic bytes being added. Preserve that section under GROVE_V4 while retaining exact GROVE_V1–V3 cost results.Three operator arms had the defect:
Basic + Sectioned,Sectioned + Basic, andBasic += Sectioned.Sectioned += Basicwas already correct and remains unchanged on every version.Version selection and scope
grovedb_versions.storage_costs.add_basic_storage_removal_to_sectioned_storage_removal, set to0for V1–V3 and1for V4.!Sendand!Sync, so its destructor restores the originating thread's selector. Mark itmust_useand document synchronous scope use, reverse-order drops for nested guards, and the prohibition on holding it across.await. The closure helper does not extend its scope to futures returned by the closure.Aggregation outside a GroveDB call, including Drive's
combine_cost_operations, still uses legacy arithmetic unless the consumer installs the guard using the applicable GroveVersion's storage-cost slot. That downstream integration remains a separate change; upgrading GroveDB alone does not correct every external cost sum.Production impact
Drive's storage-flags callback returns basic/basic removals for unflagged elements and sectioned/sectioned removals for flagged elements, with no-removal results where appropriate for zero bytes. Mixed basic/sectioned arithmetic arises when those results are aggregated. The GroveDB deletion tests deliberately use custom basic/sectioned callbacks to exercise the affected arithmetic.
Only the default identifier's section is dropped. Drive separates that section into
FeeResult.removed_bytes_from_systembefore calculating identity refunds; identity-owned sections survive in both legacy and corrected arithmetic. The 0-of-155 and 0-of-302 test results below demonstrate missing removal accounting, not missing identity refunds on each Drive deletion. See Platform's fee conversion. Preserving V1–V3 outputs maintains historical cost compatibility without asserting a demonstrated identity-refund or consensus defect.Validation
UNKNOWN_EPOCH, present without it, and absent. They preserve other owners and epochs, pin legacy losses, and assert thatSectioned += Basicis identical unguarded, under version 0, and under version 1.0/0/0/1for V1–V4.Sync. Runtime tests check thread isolation, nesting, restoration to legacy, and restoration after a nested panic.Local checks: