docs(audit): record six verified-rejected findings; correct the #693 invariant - #838
docs(audit): record six verified-rejected findings; correct the #693 invariant#838QuantumExplorer wants to merge 1 commit into
Conversation
…invariant Closes #691, #700, #702, #703, #682, #721 — all triaged as not reachable, by-design, or hygiene, and closed on GitHub with the same rationales. Adds two sections: - Trust-boundary findings (#682 lazy-loaded Merk metadata, #700 delete_checkpoint) — corrupt local storage and caller-chosen paths are inside the trust boundary; integrity auditing is delegated to Merk::verify / verify_grovedb on purpose. The #682 entry also records the argument against "fixing" it: validating hash_for_link on every link load adds hash_node_calls to the hot read path, and cost changes are replay-critical. - Version-gating and build hygiene (#702 GroveVersion::default, #703 missing version gates, #721 grovedbg build download). #691 joins the existing proof/encoding section: a dense root-only proof does verify in isolation, but every in-repo verifier either binds it to a query or discards entries and forces the root to the parent-committed hash. Also corrects a factual error that several entries leaned on. The overflow section claimed element counts are "hash-verified before the arithmetic runs". They are not: the non-Merk lower-layer verifiers are dispatched with an Element decoded from the proof's own value_bytes, and combine_hash binds it only afterwards. #693 stays closed on impact — the release build wraps and the chain check rejects the result — but the stated reason was wrong, and a build with overflow-checks enabled panics rather than wrapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 22 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
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 |
QuantumExplorer
left a comment
There was a problem hiding this comment.
Found three documentation-correctness issues. Most importantly, the corrected #693 rationale still identifies the wrong verifier invariant: the validated bulk-tree height bounds completed_chunks below the multiplication overflow threshold, so the asserted release wrap and overflow-check panic cannot occur. The transaction and optional-dependency statements also need narrower wording. Focused bulk-append, dense-tree, and version tests passed (367 tests), and git diff --check passed.
| decoded from the proof's own `value_bytes`, and only at `verify.rs:2018` | ||
| does `combine_hash(value_hash(value_bytes), &lower_hash)` check it against | ||
| the parent-committed hash. A forged count therefore *does* reach the math; | ||
| what makes that safe is that the release build wraps and the chain check |
There was a problem hiding this comment.
[P2] Use the actual verifier bound for #693
verify_and_compute_root rejects height outside 1..=16 before this calculation. Therefore chunk_item_count = 2^height is at least 2, so for every u64 total_count, completed_chunks <= floor(u64::MAX / 2) = 2^63 - 1. Consequently 2 * completed_chunks - popcount(completed_chunks) cannot overflow. Neither release wrapping nor an overflow-checks = true panic is reachable on this verifier path. Please document this bound and remove the wrap/panic assertions; as written, the audit guide records behavior that cannot occur and the unchanged “Becomes real if” rule immediately matches the newly described unauthenticated input.
| | Issue | Claim | Why not real | | ||
| |---|---|---| | ||
| | [#702](https://github.com/dashpay/grovedb/issues/702) (closed) | `GroveVersion::default` creates protocol version 0, satisfying v0 gates | Inert on two independent grounds. (1) `protocol_version` is **never read by any logic in the workspace** — grep finds it only in the four version-constant definitions and in test assertions; every gate reads a *feature* slot such as `grove_version.grovedb_versions.operations.insert.*`. (2) `GroveVersion::default()` is behaviorally identical to `GROVE_V1`: every `FeatureVersion` in V1 is `0`, and the only non-zero values in `v1.rs` are `protocol_version: 1` (never read) and `max_aggregate_sum_query_elements_scanned: 1024`, which is not a version slot and whose hand-written `impl Default` returns the same 1024. So the worst outcome is "the caller got V1 behavior", which is already legal via an explicit `GROVE_V1`. | | ||
| | [#703](https://github.com/dashpay/grovedb/issues/703) (closed) | Public versioned APIs miss explicit version gates and could execute writes instead of returning `VersionError` | Three of the seven cited refs are stale line numbers resolving to `root_key`, `root_hash` and `verify_grovedb` — two accessors and a verification helper, none version-dependent, none a write path; gating them would break callers on any unrelated slot bump. The four typed non-Merk append entry points (`mmr_tree_append`, `bulk_append`, `dense_tree_insert`, `commitment_tree_insert`) do lack a *top-level* gate but already fail closed deeper and **before any mutation**, via their cost dispatchers (`grovedb-merkle-mountain-range/src/cost/mod.rs:88-98`, `grovedb-bulk-append-tree/src/cost/mod.rs:182-201`, `grovedb-dense-fixed-sized-merkle-tree/src/tree/root_maintenance/mod.rs:56-71`, `grovedb-commitment-tree/src/commitment_tree/cost/mod.rs:43,73`). Writes also land in a `StorageBatch` committed only on success. The genuine residual is narrower and different: those four types have no `element_creation` gate of the kind `PrivateDocumentStore` uses (`grovedb_versions.rs:180-186`, enabled only in `v4.rs:407`) — a design-consistency question about when they become *creatable*, worth filing separately if wanted. | |
There was a problem hiding this comment.
[P2] Qualify the StorageBatch atomicity claim
The direct typed append operations commit their child data_batch into the transaction before updating the parent Merk; the source comments explicitly say a later parent-update failure leaves orphaned subtree data in the transaction and that a caller-supplied transaction must be rolled back. For example, mmr_tree_append can pass the MMR cost gate, stage and commit its child writes, and then fail a later parent insert_subtree version gate. The early lower-layer dispatcher supports the narrow argument, but “committed only on success” is not generally true. Please qualify this to locally owned transactions/the caller rollback contract, or remove the sentence and narrow the “Becomes real if” condition.
| |---|---|---| | ||
| | [#702](https://github.com/dashpay/grovedb/issues/702) (closed) | `GroveVersion::default` creates protocol version 0, satisfying v0 gates | Inert on two independent grounds. (1) `protocol_version` is **never read by any logic in the workspace** — grep finds it only in the four version-constant definitions and in test assertions; every gate reads a *feature* slot such as `grove_version.grovedb_versions.operations.insert.*`. (2) `GroveVersion::default()` is behaviorally identical to `GROVE_V1`: every `FeatureVersion` in V1 is `0`, and the only non-zero values in `v1.rs` are `protocol_version: 1` (never read) and `max_aggregate_sum_query_elements_scanned: 1024`, which is not a version slot and whose hand-written `impl Default` returns the same 1024. So the worst outcome is "the caller got V1 behavior", which is already legal via an explicit `GROVE_V1`. | | ||
| | [#703](https://github.com/dashpay/grovedb/issues/703) (closed) | Public versioned APIs miss explicit version gates and could execute writes instead of returning `VersionError` | Three of the seven cited refs are stale line numbers resolving to `root_key`, `root_hash` and `verify_grovedb` — two accessors and a verification helper, none version-dependent, none a write path; gating them would break callers on any unrelated slot bump. The four typed non-Merk append entry points (`mmr_tree_append`, `bulk_append`, `dense_tree_insert`, `commitment_tree_insert`) do lack a *top-level* gate but already fail closed deeper and **before any mutation**, via their cost dispatchers (`grovedb-merkle-mountain-range/src/cost/mod.rs:88-98`, `grovedb-bulk-append-tree/src/cost/mod.rs:182-201`, `grovedb-dense-fixed-sized-merkle-tree/src/tree/root_maintenance/mod.rs:56-71`, `grovedb-commitment-tree/src/commitment_tree/cost/mod.rs:43,73`). Writes also land in a `StorageBatch` committed only on success. The genuine residual is narrower and different: those four types have no `element_creation` gate of the kind `PrivateDocumentStore` uses (`grovedb_versions.rs:180-186`, enabled only in `v4.rs:407`) — a design-consistency question about when they become *creatable*, worth filing separately if wanted. | | ||
| | [#721](https://github.com/dashpay/grovedb/issues/721) (closed) | `grovedbg` build script downloads a release artifact at build time | Acceptable posture. Gated off by default (`default = ["full", "estimated_costs"]`; with `grovedbg` off, `build.rs` compiles to a literal no-op and `reqwest`/`sha2` are not even resolved), and integrity is enforced by a pinned version tag plus a pinned `GROVEDBG_SHA256` asserted at `build.rs:33`. A substituted, tampered, or 404 artifact fails the build — fail-closed. Real but minor DX defect not named in the issue: the `if !grovedbg_zip_path.exists()` guard at `:15` caches a bad download, so every later rebuild fails the SHA assert without re-fetching until `target/` is cleared. | |
There was a problem hiding this comment.
[P3] Say optional dependencies are not activated, not unresolved
Cargo still resolves optional dependencies while producing the workspace lockfile even when their features are disabled. A feature-off focused test run here generated a Cargo.lock containing both reqwest and sha2; they are absent from the active feature-off build graph, so the accurate claim is that they are not activated, built, or executed. “Not even resolved” overstates the default-feature isolation.
Follows the pattern set by #769: when an audit finding is adversarially verified and rejected, the rationale gets checked in so the next audit doesn't re-file it.
Six issues were triaged against current
developand closed on GitHub with these same rationales: #691, #700, #702, #703, #682, #721.New sections
Trust-boundary findings — #682 (lazy-loaded Merk metadata) and #700 (
delete_checkpoint). Corrupt local storage and caller-chosen paths are inside GroveDB's trust boundary; integrity auditing is delegated toMerk::verify/verify_grovedbdeliberately. The #682 entry also records the argument against fixing it: validatinghash_for_linkon every link load addshash_node_callsto the hot read path, and cost changes are replay-critical. It further notes that thedebug_assert_eq!atmerk/src/tree/mod.rs:1606is tautological and can never fire, sinceTreeNode::decodeoverwrites the decoded key with the lookup key.Version-gating and build hygiene — #702 (
GroveVersion::default:protocol_versionis never read anywhere, anddefault()is behaviorally identical toGROVE_V1), #703 (three of seven refs are read-only APIs; the four typed append entry points already fail closed before any mutation, via their cost dispatchers), #721 (non-default feature, pinned version and pinned SHA-256, fail-closed).#691 joins the existing proof/encoding section: a dense root-only proof does verify in isolation, but every in-repo verifier either binds it to a query or discards entries and forces the root to the parent-committed hash.
Correction worth reviewing
The overflow section claimed element counts are "hash-verified before any arithmetic runs". That invariant does not hold, and several entries leaned on it.
For the non-Merk lower layers the ordering is reversed:
verify.rs:1953-2016dispatches the MMR / bulk-append / dense verifiers using anElementdecoded from the proof's ownvalue_bytes, and only atverify.rs:2018doescombine_hash(value_hash(value_bytes), &lower_hash)bind it to the parent-committed hash. So a forged count genuinely does reach2 * leaf_count - popcount.#693 stays closed on impact — the release build wraps and the subsequent chain check rejects the result — but the stated reason was wrong. Worth flagging two consequences: a build with
overflow-checks = truepanics rather than wrapping, andElement::CommitmentTreeroutes through the same function, which is live on mainnet. Both the preamble bullet and the #693 row now state the real invariant, so a future refactor that turns a wrap into a hard error doesn't get waved through on a premise that was never true.Docs only — no code changes.
🤖 Generated with Claude Code