Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions grovedb/src/operations/proof/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use grovedb_merk::{
element::tree_type::ElementTreeTypeExtensions,
proofs::{
execute,
query::{PathKey, QueryProofVerify, VerifyOptions},
query::{PathKey, QueryProofVerify, VerifyOptions, PROOF_VERSION_LATEST},
Decoder, Node, Op, Query,
},
tree::{combine_hash, value_hash, NULL_HASH},
Expand Down Expand Up @@ -446,7 +446,12 @@ impl GroveDb {
};

let (root_hash, merk_result) = level_query
.execute_proof(merk_proof_bytes, *limit_left, internal_query.left_to_right)
.execute_proof(
merk_proof_bytes,
*limit_left,
internal_query.left_to_right,
PROOF_VERSION_LATEST, // V1 proof: strict mode rejects items in value hash nodes
)
.unwrap()
.map_err(|e| {
Error::InvalidProof(
Expand Down Expand Up @@ -1389,6 +1394,7 @@ impl GroveDb {
&layer_proof.merk_proof,
*limit_left,
internal_query.left_to_right,
0, // V0 proofs: allow items in value hash nodes for backwards compatibility
)
.unwrap()
.map_err(|e| {
Expand Down Expand Up @@ -1973,7 +1979,12 @@ impl GroveDb {

// Execute the proof to verify and get the root hash
let (layer_root_hash, result) = key_query
.execute_proof(&current_layer.merk_proof, None, true)
.execute_proof(
&current_layer.merk_proof,
None,
true,
0, // V0 proof: allow items in value hash nodes
)
.unwrap()
.map_err(|e| {
Error::InvalidProof(
Expand Down
16 changes: 6 additions & 10 deletions grovedb/src/tests/sum_tree_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1863,19 +1863,15 @@ mod tests {
);

// This hardcoded proof was generated by an older version of the code that
// used KVValueHash nodes for item elements. The new proof format correctly
// uses KV nodes for items (verifier-computed hash) and rejects KVValueHash
// for items to prevent KV→KVValueHash proof forgery attacks.
// used KVValueHash nodes for item elements. V0 proofs allow this for
// backwards compatibility (the KV→KVValueHash forgery check is only
// enforced for V1 proofs).
let result =
GroveDb::verify_subset_query_get_parent_tree_info(&proof, &path_query, grove_version);
assert!(
result.is_err(),
"old-format proof with KVValueHash for items should be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("must not contain an item element"),
"expected item rejection, got: {err}"
result.is_ok(),
"V0 proof with KVValueHash for items should be accepted for backwards compatibility, got: {:?}",
result.unwrap_err()
);
}

Expand Down
14 changes: 8 additions & 6 deletions merk/src/proofs/query/merk_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use super::{
*,
};
use crate::{
proofs::query::verify,
proofs::query::verify::{self, PROOF_VERSION_LATEST},
test_utils::make_tree_seq,
tree::{NoopCommit, PanicSource, RefWalker, TreeNode},
TreeFeatureType::BasicMerkNode,
Expand Down Expand Up @@ -3821,7 +3821,7 @@ fn test_5_item_tree_tampering_detected_with_elements() {
query2.insert_key(vec![1]);

let (tampered_root, _tampered_result) = query2
.execute_proof(tampered.as_slice(), None, true)
.execute_proof(tampered.as_slice(), None, true, PROOF_VERSION_LATEST)
.unwrap()
.expect("execute_proof failed");

Expand Down Expand Up @@ -3994,7 +3994,7 @@ fn test_5_item_tree_tampering_detected_raw_values() {
query2.insert_key(vec![1]);

let (tampered_root, _tampered_result) = query2
.execute_proof(tampered.as_slice(), None, true)
.execute_proof(tampered.as_slice(), None, true, PROOF_VERSION_LATEST)
.unwrap()
.expect("execute_proof failed");

Expand Down Expand Up @@ -4123,7 +4123,7 @@ fn test_tampering_detected_invalid_discriminant() {

// Use execute_proof to get the computed root
let (tampered_root, _tampered_result) = query2
.execute_proof(tampered.as_slice(), None, true)
.execute_proof(tampered.as_slice(), None, true, PROOF_VERSION_LATEST)
.unwrap()
.expect("execute_proof failed");

Expand Down Expand Up @@ -4241,7 +4241,7 @@ fn test_kvvaluehash_still_used_for_tree_discriminants() {
query2.insert_key(key.clone());
}
let result = query2
.execute_proof(tampered.as_slice(), None, true)
.execute_proof(tampered.as_slice(), None, true, PROOF_VERSION_LATEST)
.unwrap();

assert!(
Expand Down Expand Up @@ -4295,7 +4295,9 @@ fn test_execute_proof_rejects_trailing_bytes() {
// Append trailing garbage bytes
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);

let result = query.execute_proof(bytes.as_slice(), None, true).unwrap();
let result = query
.execute_proof(bytes.as_slice(), None, true, PROOF_VERSION_LATEST)
.unwrap();

assert!(
result.is_err(),
Expand Down
2 changes: 1 addition & 1 deletion merk/src/proofs/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub use map::{Map, MapBuilder};
#[cfg(any(feature = "minimal", feature = "verify"))]
pub use verify::{
ProofVerificationResult, ProvedKeyOptionalValue, ProvedKeyValue, QueryProofVerify,
VerifyOptions,
VerifyOptions, PROOF_VERSION_LATEST,
};
#[cfg(feature = "minimal")]
use {super::Op, std::collections::LinkedList};
Expand Down
96 changes: 63 additions & 33 deletions merk/src/proofs/query/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ use crate::{
CryptoHash as MerkHash, CryptoHash,
};

/// The latest proof version.
/// - V0 (0): lenient — permits item elements in KVValueHash nodes
/// (backwards compatibility with older proofs)
/// - V1 (1): strict — rejects item elements in KVValueHash /
/// KVValueHashFeatureType / KVValueHashFeatureTypeWithChildHash nodes
/// to prevent KV-to-KVValueHash proof forgery
pub const PROOF_VERSION_LATEST: u16 = 1;

/// Verify proof against expected hash
#[cfg(feature = "minimal")]
#[deprecated]
Expand Down Expand Up @@ -75,11 +83,19 @@ impl Default for VerifyOptions {
pub trait QueryProofVerify {
/// Verifies the encoded proof with the given query, returning the root
/// hash and verification result.
///
/// `proof_version` controls which security checks are applied:
/// - V0 (0): lenient — permits item elements in KVValueHash nodes
/// (backwards compatibility with older proofs)
/// - V1+ (≥1): strict — rejects item elements in KVValueHash /
/// KVValueHashFeatureType / KVValueHashFeatureTypeWithChildHash nodes
/// to prevent KV-to-KVValueHash proof forgery
fn execute_proof(
&self,
bytes: &[u8],
limit: Option<u16>,
left_to_right: bool,
proof_version: u16,
) -> CostResult<(MerkHash, ProofVerificationResult), Error>;
Comment on lines 93 to 99
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid making execute_proof a breaking public API with a bare bool.

This changes a public trait method’s signature, so every downstream caller now has to update call sites, and the new last argument is security-sensitive but opaque (..., true, false). Please preserve the old signature and add an opt-in variant or options/enum-based API for the V0 compatibility path instead.

Also applies to: 114-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@merk/src/proofs/query/verify.rs` around lines 84 - 90, The trait method
execute_proof currently changes the public signature by adding a bare bool
(left_to_right / allow_items_in_value_hash_nodes); revert the trait to preserve
the original public signature and introduce a non-breaking opt-in API: add
either an options enum/struct (e.g., ExecuteProofOptions) or a new method (e.g.,
execute_proof_with_options or execute_proof_v1) that takes the extra flags,
update implementations to implement the new method while providing a default
execute_proof that delegates to the new method with V0-compatible defaults, and
ensure the trait docs note the security-sensitive flags so callers must opt into
them explicitly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is Claude. Disagree — execute_proof is an internal trait (QueryProofVerify) that lives in the grovedb-merk crate. It is not part of any public SDK API. The only callers are:

  • 3 sites inside grovedb/src/operations/proof/verify.rs (GroveDB's own proof verification)
  • 5 sites in merk/src/proofs/query/merk_integration_tests.rs (tests)
  • 1 site in verify_proof which delegates internally

There are no downstream/external consumers of this trait. Adding an options struct or a second method for a single boolean that only has 2 internal callers passing true would be over-engineering. The parameter is well-documented in the trait doc comment explaining exactly when to use true vs false.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if QueryProofVerify is re-exported from lib.rs or any public module
rg -n "QueryProofVerify" --type rust

# Check pub use in lib.rs files
rg -n "pub use" merk/src/lib.rs 2>/dev/null || true
rg -n "pub use" merk/src/proofs/mod.rs 2>/dev/null || true
rg -n "pub use" merk/src/proofs/query/mod.rs 2>/dev/null || true

Unrecoverable error: Failed to clone repository


@QuantumExplorer, that's a fair point. The method already accepted a bare bool for left_to_right, so the new allow_items_in_value_hash_nodes is consistent with the existing style, and with only ~9 internal call sites (no external consumers), introducing an options struct would genuinely be over-engineering. The trait doc comment covers the security intent clearly. I'll withdraw the concern.


✏️ Learnings added
Learnt from: QuantumExplorer
Repo: dashpay/grovedb PR: 643
File: merk/src/proofs/query/verify.rs:84-90
Timestamp: 2026-03-10T10:18:18.519Z
Learning: In the `grovedb-merk` crate, `QueryProofVerify::execute_proof` in `merk/src/proofs/query/verify.rs` is an internal trait with no downstream/external consumers. All callers are within `grovedb/src/operations/proof/verify.rs` (3 sites), `merk/src/proofs/query/merk_integration_tests.rs` (5 sites), and the internal `verify_proof` delegation. Adding options structs or extra methods for internal-only boolean flags on this trait is considered over-engineering.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


/// Verifies the encoded proof with the given query and expected hash.
Expand Down Expand Up @@ -109,6 +125,7 @@ impl QueryProofVerify for Query {
bytes: &[u8],
limit: Option<u16>,
left_to_right: bool,
proof_version: u16,
) -> CostResult<(MerkHash, ProofVerificationResult), Error> {
#[cfg(feature = "proof_debug")]
{
Expand Down Expand Up @@ -338,15 +355,19 @@ impl QueryProofVerify for Query {
// elements to prevent KV→KVValueHash forgery where an
// attacker substitutes a KV node with KVValueHash to inject
// a fake value while keeping the original hash.
let element_type = ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in KVValueHash node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHash node must not contain an item element".to_string(),
));
// Skipped for V0 backwards compatibility.
if proof_version >= 1 {
let element_type =
ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in KVValueHash node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHash node must not contain an item element".to_string(),
));
}
}
execute_node(key, Some(value), *value_hash, false)?;
}
Expand Down Expand Up @@ -384,16 +405,21 @@ impl QueryProofVerify for Query {
println!("Processing KVValueHashFeatureType node");
}
// Same check as KVValueHash — reject item elements.
let element_type = ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in KVValueHashFeatureType node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHashFeatureType node must not contain an item element"
.to_string(),
));
// Skipped for V0 backwards compatibility.
if proof_version >= 1 {
let element_type =
ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in KVValueHashFeatureType \
node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHashFeatureType node must not contain an item element"
.to_string(),
));
}
}
execute_node(key, Some(value), *value_hash, false)?;
}
Expand All @@ -415,19 +441,23 @@ impl QueryProofVerify for Query {
{
println!("Processing KVValueHashFeatureTypeWithChildHash node");
}
// Same element-type check as KVValueHashFeatureType
let element_type = ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in \
KVValueHashFeatureTypeWithChildHash node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHashFeatureTypeWithChildHash node must not contain \
an item element"
.to_string(),
));
// Same element-type check as KVValueHashFeatureType.
// Skipped for V0 backwards compatibility.
if proof_version >= 1 {
let element_type =
ElementType::from_serialized_value(value).map_err(|e| {
Error::InvalidProofError(format!(
"cannot determine element type in \
KVValueHashFeatureTypeWithChildHash node: {e}"
))
})?;
if element_type.has_simple_value_hash() {
return Err(Error::InvalidProofError(
"KVValueHashFeatureTypeWithChildHash node must not contain \
an item element"
.to_string(),
));
}
}
// Verify value integrity: combine_hash(H(value), child_hash) must
// equal the provided value_hash. This prevents an attacker from
Expand Down Expand Up @@ -519,7 +549,7 @@ impl QueryProofVerify for Query {
left_to_right: bool,
expected_hash: MerkHash,
) -> CostResult<ProofVerificationResult, Error> {
self.execute_proof(bytes, limit, left_to_right)
self.execute_proof(bytes, limit, left_to_right, PROOF_VERSION_LATEST)
.map_ok(|(root_hash, verification_result)| {
if root_hash == expected_hash {
Ok(verification_result)
Expand Down
Loading