fix(proofs): refuse raw reference rows in the V1 layer verifier (#862) - #920
Conversation
A `KVValueHash` / `KVValueHashFeatureType` proof node hashes only `(key, value_hash)`, and the merk-level V1 guard refuses only item elements on those forms. An attacker could therefore take an honest `KVRefValueHash(key, target, H(ref))` node, recompute the combined value hash it commits to, and re-present the row as `KVValueHash(key, <forged reference bytes>, combined)`: the Merk root still reconstructed and `verify_layer_proof_v1` consumed reference metadata nothing had authenticated. Reproduced at runtime on develop: * the forged `Reference` (attacker-chosen target path) was returned as the proven element for the key, via both `verify_query` and `verify_query_raw`; the `KVValueHashFeatureType` shape under a ProvableCountTree and a forged `ReferenceWithSumItem` behaved alike; * with a subquery at that key and the lower layer dropped, every branch of the terminal arm fell through without error and the proof verified with a populated subtree's rows missing. Fix: `verify_layer_proof_v1` rejects any raw `Reference` / `ReferenceWithSumItem` row (wrapped or not) right after deserializing it, before the element type can steer a result or a descent. An honest V1 prover rewrites every consumed reference row into a `KVRefValueHash*` node carrying the dereferenced target, so no released proof is affected and the check is not version-gated, matching the existing count-offset rejection. The rejection deliberately does not live in the merk-level guard: a reference row past the query limit legitimately stays a bare `KVValueHash` in released V1 proofs (the prover's post-pass only rewrites rows within the limit), the merk verifier walks every node, and the GroveDB row loop stops before reading it — refusing references there rejected an honest limit-4 proof in `test_mixed_level_proofs`. Recorded in docs/audit-non-issues.md and the proof-system book chapter. Tests: `unbound_reference_row_tests` — honest round trip, forged row on KVValueHash (both entry points), forged row on KVValueHashFeatureType, forged ReferenceWithSumItem, forged reference hiding a populated subtree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change rejects raw reference elements during V1 GroveDB proof verification. It adds tests for forged result rows and hidden subtrees, and documents the binding rules across Merk and GroveDB proof verification. ChangesV1 reference-row verification
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The proof verifier now fails closed for unauthenticated raw reference rows while preserving honest V1 reference proofs. No actionable merge risk remains. Sequence Diagram(s)sequenceDiagram
participant Prover
participant MerkVerifier
participant verify_layer_proof_v1
participant QueryCaller
Prover->>MerkVerifier: Submit KVValueHash reference row
MerkVerifier->>verify_layer_proof_v1: Pass admitted proof row
verify_layer_proof_v1->>verify_layer_proof_v1: Reject raw Reference element
verify_layer_proof_v1-->>QueryCaller: Return InvalidProof
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 5 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 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 #920 +/- ##
===========================================
- Coverage 92.39% 92.39% -0.01%
===========================================
Files 302 302
Lines 93431 93444 +13
===========================================
+ Hits 86329 86334 +5
- Misses 7102 7110 +8
🚀 New features to boost your workflow:
|
|
This is Claude. Re-requesting the CodeRabbit review now that the rate limit window has reset. @coderabbitai review |
|
Your plan includes PR reviews subject to rate limits. More reviews will be available in 47 minutes. |
… forgery test Since #862 a raw reference row is refused as soon as it is deserialized, ahead of the arm chain's last-resort "neither descended into nor bound" rejection that #880 added. The forgery is rejected either way; the assertion now accepts both messages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
This is Claude. The branch now includes develop (#880/#917/#919 merged) at fd70370; re-requesting the CodeRabbit review on the current head. @coderabbitai review |
|
|
Closes #862 (audit P14).
Finding was real — reproduced at runtime on
developA
KVValueHash/KVValueHashFeatureTypeproof node hashes only(key, value_hash). The merk-level V1 guard from #553 refuses only item elements on those forms, so reference-family bytes pass through unbound. Taking an honestKVRefValueHash(key, target, H(ref))node, recomputingcombine_hash(H(ref), H(target)), and re-presenting the row asKVValueHash(key, <forged reference bytes>, combined)keeps the Merk root intact.verify_layer_proof_v1then consumed reference metadata nothing had authenticated:Reference(attacker-chosen target path) was returned as the proven element for the key, through bothverify_queryandverify_query_raw. TheKVValueHashFeatureTypeshape under aProvableCountTreeand a forgedReferenceWithSumItembehaved the same.#874 (#858) does not cover this: it binds what the honest prover commits for a reference, not what a forged node form can present.
Fix
verify_layer_proof_v1rejects any rawReference/ReferenceWithSumItemrow (wrapped or not) immediately after deserializing it, before the element type can steer a result or a descent. An honest V1 prover rewrites every row the verifier consumes into aKVRefValueHash*node carrying the dereferenced target, so no released proof is affected. The check is therefore not version-gated, matching the existing ungated count-offset rejection (#817) and the #553/#630/#633 pattern. V0 prover/verifier untouched.Why not in the merk-level guard
I first added the rejection to the merk
execute_proofguard at proof version 1 and it broke an honest proof (test_mixed_level_proofs, limit 4): a reference row past the query limit legitimately stays a bareKVValueHashin released V1 proofs, because the prover's post-pass only rewrites rows within the limit. The merk verifier walks every node, while the GroveDB row loop stops before reading that node. So the row consumer is the right place; the merk guard now documents why references pass there, and the invariant is recorded indocs/audit-non-issues.mdso it isn't re-filed.Tests
grovedb/src/tests/unbound_reference_row_tests.rs:KVValueHashrejected (verify_queryandverify_query_raw)KVValueHashFeatureType(ProvableCountTree parent) rejectedReferenceWithSumItemrow rejectedAll four forgeries verified successfully before the fix.
cargo test -p grovedb-merk(726) andcargo test -p grovedb --features full,verify,unsafe-dump-load(3032) pass;cargo fmtclean; clippy clean on touched files.Note on overlap: #880 (#869) edits the same terminal arm (adds a catch-all "neither descended nor bound" else). This check sits at the top of the row loop, so the two should compose.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation
Tests