Skip to content
235 changes: 229 additions & 6 deletions grovedb/src/operations/proof/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2339,13 +2339,78 @@ 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.
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 =>
{
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 +2644,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 +2733,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
18 changes: 17 additions & 1 deletion grovedb/src/operations/proof/indexed_axis/axis_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
//!
//! Each wrapper pins the [`IndexAxis`] and forwards; no logic lives here.

#[cfg(feature = "minimal")]
use grovedb_costs::CostResult;
use grovedb_element::indexed::IndexAxis;
use grovedb_merk::proofs::Query as MerkQuery;
#[cfg(feature = "minimal")]
use grovedb_path::SubtreePath;
#[cfg(feature = "minimal")]
use grovedb_version::version::GroveVersion;

use crate::{Error, GroveDb, TransactionArg};
#[cfg(feature = "minimal")]
use crate::TransactionArg;
use crate::{Error, GroveDb};

use super::{IndexedAxisAggregateResult, IndexedAxisPaginatedResult, IndexedAxisQueryResult};

Expand All @@ -18,6 +23,7 @@ impl GroveDb {

/// Prove the top-`k` entries of the count axis. Thin wrapper over
/// [`Self::prove_indexed_axis_top_k`] with `axis = Count`.
#[cfg(feature = "minimal")]
pub fn prove_indexed_count_top_k<'b, B, P>(
&self,
path: P,
Expand All @@ -41,6 +47,7 @@ impl GroveDb {
}

/// Prove an offset-paginated top-`k` window on the count axis.
#[cfg(feature = "minimal")]
pub fn prove_indexed_count_top_k_paginated<'b, B, P>(
&self,
path: P,
Expand All @@ -66,6 +73,7 @@ impl GroveDb {
}

/// Prove an arbitrary query against the count-axis secondary.
#[cfg(feature = "minimal")]
pub fn prove_indexed_count_query<'b, B, P>(
&self,
path: P,
Expand All @@ -90,6 +98,7 @@ impl GroveDb {

/// Prove the aggregate count of entries whose `count_value` is in
/// `[lo_count, hi_count]`.
#[cfg(feature = "minimal")]
pub fn prove_indexed_count_range_aggregate<'b, B, P>(
&self,
path: P,
Expand Down Expand Up @@ -181,6 +190,7 @@ impl GroveDb {
// ---------- sum axis ----------

/// Prove the top-`k` entries of the sum axis.
#[cfg(feature = "minimal")]
pub fn prove_indexed_sum_top_k<'b, B, P>(
&self,
path: P,
Expand All @@ -207,6 +217,7 @@ impl GroveDb {
/// Note: the secondary is a `ProvableSumTree`, which has no
/// count-bound offset primitive, so the proof size is
/// O(offset + k). Use sparingly with large offsets.
#[cfg(feature = "minimal")]
pub fn prove_indexed_sum_top_k_paginated<'b, B, P>(
&self,
path: P,
Expand All @@ -232,6 +243,7 @@ impl GroveDb {
}

/// Prove an arbitrary query against the sum-axis secondary.
#[cfg(feature = "minimal")]
pub fn prove_indexed_sum_query<'b, B, P>(
&self,
path: P,
Expand All @@ -256,6 +268,7 @@ impl GroveDb {

/// Prove the aggregate sum of entries whose `sum_value` is in
/// `[lo_sum, hi_sum]`.
#[cfg(feature = "minimal")]
pub fn prove_indexed_sum_range_aggregate<'b, B, P>(
&self,
path: P,
Expand Down Expand Up @@ -349,6 +362,7 @@ impl GroveDb {
/// Prove the top-`k` entries of the avg axis. PCPSIT-only. No
/// aggregate variant exists — averaging an average over a range is
/// not closed-form.
#[cfg(feature = "minimal")]
pub fn prove_indexed_avg_top_k<'b, B, P>(
&self,
path: P,
Expand All @@ -372,6 +386,7 @@ impl GroveDb {
}

/// Prove an offset-paginated top-`k` window on the avg axis.
#[cfg(feature = "minimal")]
pub fn prove_indexed_avg_top_k_paginated<'b, B, P>(
&self,
path: P,
Expand All @@ -397,6 +412,7 @@ impl GroveDb {
}

/// Prove an arbitrary query against the avg-axis secondary.
#[cfg(feature = "minimal")]
pub fn prove_indexed_avg_query<'b, B, P>(
&self,
path: P,
Expand Down
1 change: 1 addition & 0 deletions grovedb/src/operations/proof/indexed_axis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@

mod axis_api;
mod envelope;
#[cfg(feature = "minimal")]
mod generate;
mod verify;

Expand Down
6 changes: 5 additions & 1 deletion grovedb/src/operations/proof/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ mod aggregate_count_and_sum;
mod aggregate_sum;
#[cfg(feature = "minimal")]
mod generate;
#[cfg(feature = "minimal")]
// The prover lives in `indexed_axis::generate` and is `minimal`-gated there;
// the envelope types and verification entry points must be reachable from a
// verifier-only build (Dash Platform's `drive` crate compiles its
// proof-verification layer with `--no-default-features --features verify`).
#[cfg(any(feature = "minimal", feature = "verify"))]
pub mod indexed_axis;
/// Utility functions for proof display and conversion.
pub mod util;
Expand Down
Loading
Loading