Skip to content
27 changes: 27 additions & 0 deletions grovedb-version/src/version/grovedb_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,33 @@ pub struct GroveDBOperationsProofVersions {
pub verify_subset_query_with_absence_proof: FeatureVersion,
pub verify_query_with_chained_path_queries: FeatureVersion,
pub verify_query_get_parent_tree_info_with_options: FeatureVersion,
/// Whether a V1 proof binds the element bytes of a **terminally-reported
/// non-Merk tree** — `CommitmentTree`, `MmrTree`, `BulkAppendTree`,
/// `DenseAppendOnlyFixedSizeTree` — to the `value_hash` its parent Merk
/// commits to. "Terminal" means the query targets the tree element itself
/// and the prover emits no lower layer.
///
/// - `0` (V1..V3): the prover emits a bare `KVValueHash` node and the
/// verifier does not require a child hash. That node hashes only
/// `(key, value_hash)`, so the serialized element bytes are unbound: a
/// prover can serve a forged entry count (an inflated or deflated
/// `CommitmentTree` `total_count`, a different MMR size) alongside the
/// genuine `value_hash` and still reconstruct the correct root hash.
/// - `1` (V4+): the prover emits
/// `KVValueHashFeatureTypeWithChildHash` carrying the tree's own state
/// root, and the verifier requires it, so the merk-level
/// `combine_hash(H(value), child_hash) == value_hash` check closes the
/// loop. This is exactly the composition the parent commits, since these
/// types are written through `insert_subtree`.
///
/// Gated rather than applied unconditionally on two counts. It flips an
/// accepted/rejected outcome — an upgraded verifier rejects proofs a
/// released one accepts — and computing the state root costs the prover
/// extra storage reads and hash calls on a released path. The
/// non-Merk tree types this covers are the only elements affected;
/// non-empty **Merk** trees have required the child hash since V3 and
/// stay bound at every version.
pub terminal_non_merk_tree_child_hash: FeatureVersion,
}

#[derive(Clone, Debug, Default)]
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion {
verify_subset_query_with_absence_proof: 0,
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion {
verify_subset_query_with_absence_proof: 0,
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ pub const GROVE_V3: GroveVersion = GroveVersion {
verify_subset_query_with_absence_proof: 0,
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
12 changes: 12 additions & 0 deletions grovedb-version/src/version/v4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
//! case. Same shape as the gate above: one extra stored-element read per
//! overwrite-capable op, so V1..V3 keep their released cost shape.
//!
//! - `proof.terminal_non_merk_tree_child_hash: 1` — a V1 proof that reports a
//! `CommitmentTree` / `MmrTree` / `BulkAppendTree` /
//! `DenseAppendOnlyFixedSizeTree` as a terminal result (query targets the
//! tree element itself, no lower layer) carries the tree's state root in a
//! `KVValueHashFeatureTypeWithChildHash` node, and the verifier requires it.
//! V1..V3 emit a bare `KVValueHash`, which hashes only `(key, value_hash)`
//! and so leaves the element bytes — including the entry count callers read
//! — free for a prover to forge under a genuine root hash. Gated because it
//! flips a rejected/accepted outcome and because deriving the state root
//! costs the prover extra storage reads and hash calls.
//!
//! Note that `GroveVersion::latest()` resolves to this version, so anything
//! defaulting to "latest" — tests, benchmarks, tools — exercises every gate
//! listed above rather than V3 behaviour.
Expand Down Expand Up @@ -205,6 +216,7 @@ pub const GROVE_V4: GroveVersion = GroveVersion {
verify_subset_query_with_absence_proof: 0,
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 1, // bind terminal non-Merk tree element bytes to the parent value_hash
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
252 changes: 246 additions & 6 deletions grovedb/src/operations/proof/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2339,13 +2339,95 @@ impl GroveDb {
lower_layers.insert(key.clone(), layer_proof);
}

// MmrTree/BulkAppendTree without subquery (query targets the tree
// itself)
Ok(Element::MmrTree(..))
| Ok(Element::BulkAppendTree(..))
| Ok(Element::DenseAppendOnlyFixedSizeTree(..))
// Non-Merk tree that is itself the result, with
// nothing queried below it. These types have no
// child Merk, so there is no lower layer to bind
// them — a bare `KVValueHash` node hashes only
// (key, value_hash) and would leave the element
// bytes (and with them the entry count a caller
// reads) free for a prover to forge under a
// genuine root hash. Their parent commits
// `combine_hash(H(value), state_root)`, exactly
// the two-input form
// `KVValueHashFeatureTypeWithChildHash` is
// verified with, so carry the state root in the
// node and let the merk verifier close the loop.
//
// Version-gated on
// `proof.terminal_non_merk_tree_child_hash`:
// deriving the state root costs storage reads and
// hash calls that V1..V3 did not pay, and cost
// feeds fees. Under those versions the node is
// left as the prover has always emitted it and
// only the limit moves — the released shape, which
// the matching verifier gate still accepts.
Ok(ref non_merk_elem @ Element::MmrTree(..))
| Ok(ref non_merk_elem @ Element::BulkAppendTree(..))
| Ok(
ref non_merk_elem @ Element::DenseAppendOnlyFixedSizeTree(..),
)
| Ok(ref non_merk_elem @ Element::CommitmentTree(..))
if !done_with_results =>
{
if grove_version
.grovedb_versions
.operations
.proof
.terminal_non_merk_tree_child_hash
>= 1
{
let mut child_path = path.clone();
child_path.push(key.as_slice());

let child_hash = cost_return_on_error!(
&mut cost,
self.non_merk_tree_child_hash(
non_merk_elem,
&child_path,
&tx,
)
);

let key_owned = key.to_owned();
let value_owned = value.to_owned();
let element_vh =
value_hash(&value_owned).unwrap_add_cost(&mut cost);
let recomputed = combine_hash(&element_vh, &child_hash)
.unwrap_add_cost(&mut cost);
let (vh, ft) = match node {
Node::KVValueHashFeatureType(_, _, vh, ft) => (*vh, *ft),
Node::KVValueHash(_, _, vh) => {
(*vh, TreeFeatureType::BasicMerkNode)
}
_ => (recomputed, TreeFeatureType::BasicMerkNode),
};

// Self-check: if the recomputed state root
// does not reproduce the committed
// value_hash, the node we are about to emit
// would be rejected by the verifier. Fail
// here, where the cause is visible, rather
// than shipping a proof that cannot verify.
if recomputed != vh {
return Err(Error::CorruptedData(format!(
"non-Merk tree at key {} has state root {} which \
does not reproduce the committed value hash {}",
hex::encode(&key_owned),
hex::encode(child_hash),
hex::encode(vh),
)))
.wrap_with_cost(cost);
}

*node = Node::KVValueHashFeatureTypeWithChildHash(
key_owned,
value_owned,
vh,
ft,
child_hash,
);
}

if let Some(limit) = overall_limit.as_mut() {
*limit -= 1;
}
Expand Down Expand Up @@ -2579,7 +2661,10 @@ impl GroveDb {
}
lower_layers.insert(key.clone(), layer_proof);
}
// Empty trees and CommitmentTree without subquery
// Empty trees without subquery. CommitmentTree is
// NOT here — like the other non-Merk trees it is
// bound by the child-hash arm above, which applies
// whether or not it holds any notes.
Ok(Element::Tree(None, _))
| Ok(Element::SumTree(None, ..))
| Ok(Element::BigSumTree(None, ..))
Expand Down Expand Up @@ -2665,6 +2750,161 @@ impl GroveDb {
.wrap_with_cost(cost)
}

/// Compute the child hash that a non-Merk tree element's parent Merk
/// commits to, i.e. the `child_hash` satisfying
/// `combine_hash(H(value), child_hash) == value_hash`.
///
/// `CommitmentTree`, `MmrTree`, `BulkAppendTree` and
/// `DenseAppendOnlyFixedSizeTree` have no child Merk; their parent entry is
/// written by `insert_subtree` with the tree's own state root as the
/// supplied hash. This reproduces that hash so a terminal proof of the tree
/// element itself can carry it and stay bound to the element bytes.
///
/// Each arm must mirror the corresponding write path exactly:
/// - `MmrTree` / `DenseAppendOnlyFixedSizeTree` / `BulkAppendTree` are
/// inserted with `NULL_HASH` while still empty, and only start committing
/// a computed root once the first append lands. Note that an empty
/// `BulkAppendTree`'s `compute_current_state_root()` is *not* `NULL_HASH`,
/// so the zero-count case has to short-circuit.
/// - `CommitmentTree` is inserted with `EMPTY_COMMITMENT_TREE_STATE_ROOT`,
/// which is exactly what the sinsemilla/bulk composition below yields at
/// count 0 — no special case needed.
fn non_merk_tree_child_hash(
&self,
element: &Element,
subtree_path: &[&[u8]],
tx: &Transaction,
) -> CostResult<grovedb_merk::CryptoHash, Error> {
use grovedb_merk::tree::NULL_HASH;

let mut cost = OperationCost::default();

let path_vec: Vec<Vec<u8>> = subtree_path.iter().map(|s| s.to_vec()).collect();
let path_refs: Vec<&[u8]> = path_vec.iter().map(|v| v.as_slice()).collect();
let storage_path = grovedb_path::SubtreePath::from(path_refs.as_slice());

match element {
Element::MmrTree(mmr_size, _) => {
if *mmr_size == 0 {
return Ok(NULL_HASH).wrap_with_cost(cost);
}
let storage_ctx = self
.db
.get_transactional_storage_context(storage_path, None, tx)
.unwrap_add_cost(&mut cost);
let store = grovedb_merkle_mountain_range::MmrStore::new(&storage_ctx);
let mmr = grovedb_merkle_mountain_range::MMR::new(*mmr_size, &store);
let root = cost_return_on_error!(
&mut cost,
mmr.get_root()
.map_err(|e| Error::CorruptedData(format!("MMR get_root failed: {}", e)))
);
Ok(root.hash()).wrap_with_cost(cost)
}
Element::DenseAppendOnlyFixedSizeTree(count, height, _) => {
if *count == 0 {
return Ok(NULL_HASH).wrap_with_cost(cost);
}
let storage_ctx = self
.db
.get_transactional_storage_context(storage_path, None, tx)
.unwrap_add_cost(&mut cost);
let tree = cost_return_on_error_no_add!(
cost,
grovedb_dense_fixed_sized_merkle_tree::DenseFixedSizedMerkleTree::from_state(
*height,
*count,
storage_ctx,
)
.map_err(|e| Error::CorruptedData(format!("dense tree state error: {}", e)))
);
let root_hash = cost_return_on_error!(
&mut cost,
tree.root_hash().map_err(|e| Error::CorruptedData(format!(
"dense tree root hash error: {}",
e
)))
);
Ok(root_hash).wrap_with_cost(cost)
}
Element::BulkAppendTree(total_count, chunk_power, _) => {
if *total_count == 0 {
return Ok(NULL_HASH).wrap_with_cost(cost);
}
let storage_ctx = self
.db
.get_transactional_storage_context(storage_path, None, tx)
.unwrap_add_cost(&mut cost);
let tree = cost_return_on_error_no_add!(
cost,
grovedb_bulk_append_tree::BulkAppendTree::from_state(
*total_count,
*chunk_power,
storage_ctx,
)
.map_err(|e| Error::CorruptedData(format!(
"failed to create BulkAppendTree: {}",
e
)))
);
let state_root = cost_return_on_error_no_add!(
cost,
tree.compute_current_state_root().map_err(|e| {
Error::CorruptedData(format!("bulk append state root failed: {}", e))
})
);
Ok(state_root).wrap_with_cost(cost)
}
Element::CommitmentTree(total_count, chunk_power, _) => {
let storage_ctx = self
.db
.get_transactional_storage_context(storage_path, None, tx)
.unwrap_add_cost(&mut cost);

let sinsemilla_root = match storage_ctx.get(COMMITMENT_TREE_DATA_KEY).value {
Ok(Some(frontier_bytes)) => {
match grovedb_commitment_tree::CommitmentFrontier::deserialize(
frontier_bytes.as_ref(),
) {
Ok(frontier) => frontier.root_hash(),
Err(_) => grovedb_commitment_tree::EMPTY_SINSEMILLA_ROOT,
}
}
_ => grovedb_commitment_tree::EMPTY_SINSEMILLA_ROOT,
};

let tree = cost_return_on_error_no_add!(
cost,
grovedb_bulk_append_tree::BulkAppendTree::from_state(
*total_count,
*chunk_power,
storage_ctx,
)
.map_err(|e| Error::CorruptedData(format!(
"failed to create BulkAppendTree: {}",
e
)))
);
let bulk_state_root = cost_return_on_error_no_add!(
cost,
tree.compute_current_state_root().map_err(|e| {
Error::CorruptedData(format!("bulk append state root failed: {}", e))
})
);

Ok(grovedb_commitment_tree::compute_commitment_tree_state_root(
&sinsemilla_root,
&bulk_state_root,
))
.wrap_with_cost(cost)
}
_ => Err(Error::CorruptedCodeExecution(
"non_merk_tree_child_hash called on an element that is not a non-Merk tree",
))
.wrap_with_cost(cost),
}
}

/// Generate an MMR tree layer proof for a subquery.
fn generate_mmr_layer_proof(
&self,
Expand Down
Loading
Loading