fix(costs): version default-section storage removal accounting - #726
fix(costs): version default-section storage removal accounting#726QuantumExplorer wants to merge 4 commits into
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❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #726 +/- ##
===========================================
+ Coverage 92.41% 92.45% +0.04%
===========================================
Files 289 289
Lines 89316 89364 +48
===========================================
+ Hits 82537 82618 +81
+ Misses 6779 6746 -33
🚀 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>
5d827ed to
0ab0499
Compare
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>
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)
|
Summary
Fixes #683 — combining a
BasicStorageRemovalwith aSectionedStorageRemovaldropped the default identifier's section, undercounting removed bytes (and therefore refunds).The bug. In
StorageRemovedBytes, three of the four basic-into-sectioned arms —Add: Basic + Sectioned,Add: Sectioned + Basic,AddAssign: Basic += Sectioned— removed the default identifier's epoch map from the sectioned map, folded the basic bytes into itsUNKNOWN_EPOCHentry, and then never reinserted it. The whole default section (not just the basic bytes) was lost. The fourth arm,AddAssign: Sectioned += Basic, always reinserted correctly. The shape Drive's sectional-removal callback produces (key bytes asBasic, value bytes asSectionedunder the default identifier) hits the brokenBasic += Sectionedarm every time Merk folds a deletion's key and value removals together, so a full deletion refunded 0 bytes.The fix, gated to GROVE_V4. GROVE_V3 is live on mainnet with the buggy arithmetic and removal totals feed fee refunds, so historical blocks must replay to the same (wrong) figures.
grovedb_versions.storage_costs.add_basic_storage_removal_to_sectioned_storage_removal:0on V1/V2/V3 (legacy, byte-identical to shipped),1on V4 (default section preserved).Add/AddAssignoperator impls ingrovedb-costs, reached through hundreds of version-lessadd_cost/cost_return_on_error!sites, andgrovedb-costshas nogrovedb-versiondependency. So the slot is carried by a thread-local selector (Cell<u16>) with an RAII guard (use_basic_sectioned_removal_addition_version/with_basic_sectioned_removal_addition_version) installed at the version-aware entry points:Merk::apply_unchecked_with_old_value_observer(innermost apply, which every Merk apply funnels through),GroveDb::delete_with_sectional_storage_function,delete_if_empty_tree_with_sectional_storage_function,apply_batch_with_element_flags_update, andapply_partial_batch_with_element_flags_update.0(legacy). An un-guarded caller reproduces shipped behaviour rather than silently upgrading — the safe failure direction.Sectioned += Basicarm bypasses the selector entirely; routing it through the gate would have regressed v1..v3 output (see the second commit).Verification
Pinned regressions (exact figures, not inequalities):
grovedb-costscoverage_regression: legacy path pins each of the three buggy arms to their shipped (dropping) totals and the always-correct arm to13;latest_storage_removed_bytes_add_preserves_default_sectionpins all four arms under version1.grovedb-version:v4_uses_fixed_basic_to_sectioned_storage_removal_additionasserts0/0/0/1across V1..V4.grovedb(batch/single_deletion_cost_tests.rs): insertkey1 → Item("cat", flags "apple")at the root (155 added bytes), delete viadelete_with_sectional_storage_functionwith key bytes asBasicand value bytes asSectioned{default: {UNKNOWN_EPOCH}}:latest_…(V4): removal isSectioned{default: {UNKNOWN_EPOCH: 155}}, total 155 = added.v3_…_keeps_legacy_…: removal isSectioned{}(empty map), total 0 of 155 — the shipped undercount, pinned exactly.Scope note / Platform follow-up
The guard covers every GroveDB/Merk path that combines removals during an operation. Removal aggregation that happens outside a GroveDB call — e.g. Drive summing per-operation costs with
+=/combine_cost_operations— still runs with the default (legacy) selector even under V4 unless Drive installs the guard itself (grovedb_costs::storage_cost::removal::use_basic_sectioned_removal_addition_version(1)) around that aggregation. That is a Platform-side follow-up; it does not affect the figures GroveDB returns.Summary by CodeRabbit