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
84 changes: 84 additions & 0 deletions grovedb/src/operations/proof/bind_terminal_non_merk_tree/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! `bind_terminal_non_merk_tree` — versioned dispatch.
//!
//! Binds the serialized 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, so there is no child layer to chain
//! through.
//!
//! These four types have no child Merk. Their parent entry is written by
//! `insert_subtree`, which commits `combine_hash(H(value), state_root)` — the
//! same two-input form that `Node::KVValueHashFeatureTypeWithChildHash` is
//! verified with. Carrying the state root in the node is therefore enough for
//! the merk verifier to close the loop; no new proof node type is needed.
//!
//! Whether it is carried is **consensus-critical** and version-gated on
//! `proof.terminal_non_merk_tree_child_hash`:
//!
//! * **[v0]** — released behaviour, `GROVE_V1`..`GROVE_V3`. The node is left
//! exactly as the prover emitted it (a bare `Node::KVValueHash`), which
//! hashes only `(key, value_hash)`. The 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.
//! * **[v1]** — `GROVE_V4`+. The tree's state root is derived from storage and
//! the node is rewritten to `KVValueHashFeatureTypeWithChildHash`, so the
//! merk verifier's `combine_hash(H(value), child_hash) == value_hash` check
//! catches forged bytes. The matching verifier gate in
//! [`verify`](super::verify) requires the node from the same version.
//!
//! The split cannot be applied unconditionally on two counts: it flips an
//! accepted/rejected outcome, and deriving the state root costs the prover
//! storage reads and hash calls that the released versions never paid — cost
//! feeds fees. See `grovedb-version`'s `v4.rs` for the landing-zone rationale.
//!
//! [v0]: self::v0
//! [v1]: self::v1

mod v0;
mod v1;

use grovedb_costs::{CostResult, CostsExt, OperationCost};
use grovedb_merk::proofs::Node;
use grovedb_version::version::GroveVersion;

use crate::{Element, Error, GroveDb, Transaction};

impl GroveDb {
/// Bind a terminally-reported non-Merk tree's element bytes to the
/// parent-committed `value_hash`, if the grove version calls for it.
///
/// `node` is the proof node standing for the tree element; `element` is
/// that node's already-deserialized (and `NonCounted`-unwrapped) value, and
/// must be one of the four non-Merk tree types. `parent_path` is the path
/// of the Merk holding the element — the tree's own data lives one level
/// below, under the node's key, which the versioned implementations append
/// themselves.
pub(crate) fn bind_terminal_non_merk_tree(
&self,
node: &mut Node,
element: &Element,
parent_path: &[&[u8]],
tx: &Transaction,
grove_version: &GroveVersion,
) -> CostResult<(), Error> {
match grove_version
.grovedb_versions
.operations
.proof
.terminal_non_merk_tree_child_hash
{
0 => self.bind_terminal_non_merk_tree_v0(node, element, parent_path, tx),
1 => self.bind_terminal_non_merk_tree_v1(node, element, parent_path, tx),
version => Err(Error::VersionError(
grovedb_version::error::GroveVersionError::UnknownVersionMismatch {
method: "bind_terminal_non_merk_tree".to_string(),
known_versions: vec![0, 1],
received: version,
},
))
.wrap_with_cost(OperationCost::default()),
}
}
}
35 changes: 35 additions & 0 deletions grovedb/src/operations/proof/bind_terminal_non_merk_tree/v0.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! `bind_terminal_non_merk_tree` — **v0** (released behaviour,
//! `GROVE_V1`..`GROVE_V3`).
//!
//! Does nothing. The proof node for a terminally-reported non-Merk tree is left
//! exactly as the prover emitted it — a bare `Node::KVValueHash`, which hashes
//! only `(key, value_hash)` and leaves the serialized element bytes unbound.
//!
//! This is a **known soundness gap**, not an oversight to fix in place: a
//! prover can serve a forged entry count alongside the genuine `value_hash` and
//! still reconstruct the correct root hash. It is preserved here because
//! `GROVE_V3` is live — closing it changes both an accepted/rejected outcome
//! and the prover's tracked cost, so nodes carrying the fix would diverge from
//! nodes that do not. [`super::v1`] closes it from `GROVE_V4` onward; the hole
//! shuts when that protocol version activates.
//!
//! Deliberately takes the same arguments as [`super::v1`] and ignores them, so
//! the dispatch in [`super`][`mod@super`] stays a plain version match.

use grovedb_costs::{CostResult, CostsExt, OperationCost};
use grovedb_merk::proofs::Node;

use crate::{Element, Error, GroveDb, Transaction};

impl GroveDb {
/// `bind_terminal_non_merk_tree` v0 — see the module documentation.
pub(crate) fn bind_terminal_non_merk_tree_v0(
&self,
_node: &mut Node,
_element: &Element,
_parent_path: &[&[u8]],
_tx: &Transaction,
) -> CostResult<(), Error> {
Ok(()).wrap_with_cost(OperationCost::default())
}
}
Loading
Loading