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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ members = [
"grovedb-merkle-mountain-range",
"grovedb-dense-fixed-sized-merkle-tree",
"grovedb-bulk-append-tree",
"grovedb-commitment-tree",
"grovedb-query",
]
7 changes: 7 additions & 0 deletions costs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub struct OperationCost {
pub storage_loaded_bytes: u64,
/// How many times node hashing was done (for merkelized tree).
pub hash_node_calls: u32,
/// How many Sinsemilla hash operations were done (elliptic curve operations
/// for commitment tree anchors). These are significantly more expensive
/// than Blake3 node hashes.
pub sinsemilla_hash_calls: u32,
}

impl OperationCost {
Expand Down Expand Up @@ -176,6 +180,7 @@ impl OperationCost {
&& self.storage_cost.worse_or_eq_than(&other.storage_cost)
&& self.storage_loaded_bytes >= other.storage_loaded_bytes
&& self.hash_node_calls >= other.hash_node_calls
&& self.sinsemilla_hash_calls >= other.sinsemilla_hash_calls
}

/// add storage_cost costs for key and value storages
Expand Down Expand Up @@ -291,6 +296,7 @@ impl Add for OperationCost {
storage_cost: self.storage_cost + rhs.storage_cost,
storage_loaded_bytes: self.storage_loaded_bytes + rhs.storage_loaded_bytes,
hash_node_calls: self.hash_node_calls + rhs.hash_node_calls,
sinsemilla_hash_calls: self.sinsemilla_hash_calls + rhs.sinsemilla_hash_calls,
}
}
}
Expand All @@ -301,6 +307,7 @@ impl AddAssign for OperationCost {
self.storage_cost += rhs.storage_cost;
self.storage_loaded_bytes += rhs.storage_loaded_bytes;
self.hash_node_calls += rhs.hash_node_calls;
self.sinsemilla_hash_calls += rhs.sinsemilla_hash_calls;
}
}

Expand Down
37 changes: 37 additions & 0 deletions grovedb-commitment-tree/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "grovedb-commitment-tree"
description = "Orchard-style commitment tree integration for GroveDB"
version = "4.0.0"
authors = ["Samuel Westrich <sam@dash.org>"]
edition = "2021"
license = "MIT"
homepage = "https://www.grovedb.org"
repository = "https://github.com/dashpay/grovedb"
readme = "../README.md"
documentation = "https://docs.rs/grovedb"

[features]
default = []
server = []
storage = ["grovedb-storage", "grovedb-bulk-append-tree", "server"]
client = ["shardtree"]
sqlite = ["client", "rusqlite"]

[dependencies]
orchard = { git = "https://github.com/dashpay/orchard.git", rev = "41c8f7169f2683c99cf0e0c63e8d25ec12c47a79", features = ["circuit"] }
incrementalmerkletree = "0.8"
shardtree = { version = "0.6", optional = true }
rusqlite = { version = "0.38", features = ["bundled"], optional = true }
grovedb-costs = { version = "4.0.0", path = "../costs" }
grovedb-storage = { version = "4.0.0", path = "../storage", optional = true }
grovedb-bulk-append-tree = { version = "4.0.0", path = "../grovedb-bulk-append-tree", optional = true }
Comment on lines +20 to +27

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify dependency version compatibility

echo "=== Checking shardtree 0.6 dependencies on crates.io ==="
curl -s "https://crates.io/api/v1/crates/shardtree/0.6.1" | jq -r '.version.deps[]' 2>/dev/null | head -20

echo -e "\n=== Checking incrementalmerkletree 0.8.x versions ==="
curl -s "https://crates.io/api/v1/crates/incrementalmerkletree" 2>/dev/null | jq -r '.versions[] | select(.num | startswith("0.8")) | .num' | sort -V

echo -e "\n=== Checking latest shardtree versions ==="
curl -s "https://crates.io/api/v1/crates/shardtree" 2>/dev/null | jq -r '.versions[0:5] | .[] | .num'

echo -e "\n=== Checking thiserror 2.x versions ==="
curl -s "https://crates.io/api/v1/crates/thiserror" 2>/dev/null | jq -r '.versions[] | select(.num | startswith("2.")) | .num' | head -5

Repository: dashpay/grovedb

Length of output: 338


🏁 Script executed:

#!/bin/bash
# Check shardtree's actual dependency requirements

echo "=== Checking shardtree 0.6.1 full crate data ==="
curl -s "https://crates.io/api/v1/crates/shardtree/0.6.1" | jq '.version' 2>/dev/null

echo -e "\n=== Checking shardtree 0.6.1 dependencies directly ==="
curl -s "https://crates.io/api/v1/crates/shardtree/0.6.1/dependencies" 2>/dev/null | jq '.'

echo -e "\n=== Alternative: checking raw Cargo.toml from GitHub ==="
curl -s "https://raw.githubusercontent.com/zcash/incrementalmerkletree/main/shardtree/Cargo.toml" 2>/dev/null | grep -A 5 "\[dependencies\]"

Repository: dashpay/grovedb

Length of output: 5780


🏁 Script executed:

cat grovedb-commitment-tree/Cargo.toml | sed -n '20,30p'

Repository: dashpay/grovedb

Length of output: 637


Pin incrementalmerkletree to at least 0.8.1 for shardtree 0.6 compatibility.

The current constraint incrementalmerkletree = "0.8" allows version 0.8.0, but shardtree = { version = "0.6", optional = true } requires incrementalmerkletree >= 0.8.1. Change this to incrementalmerkletree = "0.8.1" to ensure the correct version is selected and avoid potential dependency resolution issues.

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

In `@grovedb-commitment-tree/Cargo.toml` around lines 20 - 27, Update the
incrementalmerkletree dependency to ensure compatibility with shardtree: change
the version constraint for incrementalmerkletree in Cargo.toml from "0.8" to
"0.8.1" so that incrementalmerkletree satisfies shardtree (shardtree = { version
= "0.6", optional = true }) which requires >= 0.8.1; modify the line referencing
incrementalmerkletree to "0.8.1".

thiserror = "2.0"

[dev-dependencies]
tempfile = "3"
criterion = "0.4"
rand = "0.8"

[[bench]]
name = "verification"
harness = false
150 changes: 150 additions & 0 deletions grovedb-commitment-tree/benches/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Benchmarks for Orchard proof and signature verification.
//!
//! Measures the actual time ratios between:
//! - Halo 2 ZK proof verification (per-bundle, scales with action count)
//! - RedPallas spend auth signature verification (per-action)
//! - RedPallas binding signature verification (per-bundle)
//! - Full BatchValidator (proof + all signatures)
//!
//! Run with:
//! ```
//! cargo bench -p grovedb-commitment-tree --bench verification
//! ```
//!
//! NOTE: The first run takes ~35 seconds to build ProvingKey + VerifyingKey.

use std::sync::OnceLock;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use grovedb_commitment_tree::{
Anchor, Authorized, BatchValidator, Builder, Bundle, BundleType, DashMemo, Flags,
FullViewingKey, Hashable, MerkleHashOrchard, NoteValue, ProvingKey, Scope, SpendingKey,
VerifyingKey,
};
use rand::rngs::OsRng;

static PROVING_KEY: OnceLock<ProvingKey> = OnceLock::new();
static VERIFYING_KEY: OnceLock<VerifyingKey> = OnceLock::new();

fn get_pk() -> &'static ProvingKey {
PROVING_KEY.get_or_init(ProvingKey::build)
}

fn get_vk() -> &'static VerifyingKey {
VERIFYING_KEY.get_or_init(VerifyingKey::build)
}

/// Build a shielding bundle (output-only, SPENDS_DISABLED) with N outputs = N
/// actions.
fn build_bundle(num_outputs: usize) -> Bundle<Authorized, i64, DashMemo> {
let mut rng = OsRng;
let pk = get_pk();

let sk = SpendingKey::from_bytes([7; 32]).unwrap();
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, Scope::External);

let anchor: Anchor = MerkleHashOrchard::empty_root(32.into()).into();

let mut builder = Builder::<DashMemo>::new(
BundleType::Transactional {
flags: Flags::SPENDS_DISABLED,
bundle_required: false,
},
anchor,
);

for _ in 0..num_outputs {
builder
.add_output(None, recipient, NoteValue::from_raw(5000), [0u8; 36])
.unwrap();
}

let (unauthorized, _) = builder.build::<i64>(&mut rng).unwrap().unwrap();
let sighash: [u8; 32] = unauthorized.commitment().into();
let proven = unauthorized.create_proof(pk, &mut rng).unwrap();
proven.apply_signatures(rng, sighash, &[]).unwrap()
}

fn benchmark_proof_verification(c: &mut Criterion) {
let vk = get_vk();

// Pre-build bundles with 1-4 actions
let bundles: Vec<_> = (1..=4).map(build_bundle).collect();

// --- Halo 2 proof verification (scales with action count) ---
{
let mut group = c.benchmark_group("halo2_proof");
group.sample_size(10);
for (i, bundle) in bundles.iter().enumerate() {
let num_actions = i + 1;
let instances: Vec<_> = bundle
.actions()
.iter()
.map(|a| a.to_instance(*bundle.flags(), *bundle.anchor()))
.collect();

group.bench_function(BenchmarkId::new("actions", num_actions), |b| {
b.iter(|| bundle.authorization().proof().verify(vk, &instances));
});
}
group.finish();
}
}

fn benchmark_signature_verification(c: &mut Criterion) {
let bundle = build_bundle(1);
let sighash: [u8; 32] = bundle.commitment().into();

// --- RedPallas spend auth signature (per-action cost) ---
{
let action = &bundle.actions()[0];
let rk = action.rk();
let sig = action.authorization();

c.bench_function("redpallas_spend_auth_sig", |b| {
b.iter(|| rk.verify(&sighash, sig));
});
}

// --- RedPallas binding signature (per-bundle cost) ---
{
let bvk = bundle.binding_validating_key();
let binding_sig = bundle.authorization().binding_signature();

c.bench_function("redpallas_binding_sig", |b| {
b.iter(|| bvk.verify(&sighash, binding_sig));
});
}
}

fn benchmark_batch_validator(c: &mut Criterion) {
let vk = get_vk();

let bundles: Vec<_> = (1..=4).map(build_bundle).collect();

// --- Full BatchValidator: proof + spend auth sigs + binding sig ---
let mut group = c.benchmark_group("batch_validator");
group.sample_size(10);
for (i, bundle) in bundles.iter().enumerate() {
let num_actions = i + 1;
let sighash: [u8; 32] = bundle.commitment().into();

group.bench_function(BenchmarkId::new("actions", num_actions), |b| {
b.iter(|| {
let mut validator = BatchValidator::new();
validator.add_bundle(bundle, sighash);
validator.validate(vk, OsRng)
});
});
}
group.finish();
}

criterion_group!(
benches,
benchmark_proof_verification,
benchmark_signature_verification,
benchmark_batch_validator,
);
criterion_main!(benches);
121 changes: 121 additions & 0 deletions grovedb-commitment-tree/src/client/client_memory_commitment_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use incrementalmerkletree::{Position, Retention};
use orchard::{
tree::{MerkleHashOrchard, MerklePath},
Anchor, NOTE_COMMITMENT_TREE_DEPTH,
};
use shardtree::{store::memory::MemoryShardStore, ShardTree};

use super::SHARD_HEIGHT;
use crate::commitment_frontier::{merkle_hash_from_bytes, CommitmentTreeError};

/// Client-side Orchard commitment tree with full Merkle witness support.
///
/// Wraps `ShardTree<MemoryShardStore<MerkleHashOrchard, u32>, 32, 4>` with
/// a convenient Orchard-typed API. All state is in-memory and lost on drop.
///
/// Use this for:
/// - Wallet note tracking and spend witness generation
/// - Test harnesses that construct valid Orchard spend bundles
///
/// Do **not** use this for server-side anchor tracking — use
/// [`CommitmentFrontier`](crate::commitment_frontier::CommitmentFrontier)
/// instead.
pub struct ClientMemoryCommitmentTree {
inner: ShardTree<
MemoryShardStore<MerkleHashOrchard, u32>,
{ NOTE_COMMITMENT_TREE_DEPTH as u8 },
SHARD_HEIGHT,
>,
}

impl ClientMemoryCommitmentTree {
/// Create a new empty client commitment tree.
///
/// `max_checkpoints` controls how many checkpoints are retained before
/// the oldest are pruned.
pub fn new(max_checkpoints: usize) -> Self {
Self {
inner: ShardTree::new(MemoryShardStore::empty(), max_checkpoints),
}
}

/// Append a note commitment to the tree.
///
/// `cmx` is the 32-byte extracted note commitment. `retention` controls
/// whether the leaf is marked for witness generation, checkpointed, or
/// ephemeral.
pub fn append(
&mut self,
cmx: [u8; 32],
retention: Retention<u32>,
) -> Result<(), CommitmentTreeError> {
let leaf = merkle_hash_from_bytes(&cmx).ok_or(CommitmentTreeError::InvalidFieldElement)?;
self.inner
.batch_insert(self.next_position()?, std::iter::once((leaf, retention)))
.map_err(|e| CommitmentTreeError::InvalidData(format!("append failed: {e}")))?;
Ok(())
}

/// Create a checkpoint at the current tree state.
///
/// Checkpoints allow `witness_at_checkpoint_depth` to produce witnesses
/// relative to historical anchors.
pub fn checkpoint(&mut self, checkpoint_id: u32) -> Result<bool, CommitmentTreeError> {
self.inner
.checkpoint(checkpoint_id)
.map_err(|e| CommitmentTreeError::InvalidData(format!("checkpoint failed: {e}")))
}

/// Get the position of the most recently appended leaf.
///
/// Returns `None` if the tree is empty.
pub fn max_leaf_position(&self) -> Result<Option<Position>, CommitmentTreeError> {
self.inner
.max_leaf_position(None)
.map_err(|e| CommitmentTreeError::InvalidData(format!("max_leaf_position failed: {e}")))
}

/// Generate a Merkle witness (authentication path) for spending a note
/// at the given position.
///
/// `checkpoint_depth` is 0 for the current tree state, 1 for the
/// previous checkpoint, etc. The leaf at `position` must have been
/// inserted with `Retention::Marked` or `Retention::Checkpoint { marking:
/// Marking::Marked, .. }`.
pub fn witness(
&self,
position: Position,
checkpoint_depth: usize,
) -> Result<Option<MerklePath>, CommitmentTreeError> {
self.inner
.witness_at_checkpoint_depth(position, checkpoint_depth)
.map(|opt| opt.map(MerklePath::from))
.map_err(|e| CommitmentTreeError::InvalidData(format!("witness failed: {e}")))
}

/// Get the current root as an Orchard `Anchor`.
///
/// Returns the empty tree anchor if no leaves have been appended.
pub fn anchor(&self) -> Result<Anchor, CommitmentTreeError> {
match self
.inner
.root_at_checkpoint_depth(None)
.map_err(|e| CommitmentTreeError::InvalidData(format!("root failed: {e}")))?
{
Some(root) => Ok(Anchor::from(root)),
None => Ok(Anchor::empty_tree()),
}
}

/// Get the next insertion position (0 for empty tree).
fn next_position(&self) -> Result<Position, CommitmentTreeError> {
let pos = self
.inner
.max_leaf_position(None)
.map_err(|e| CommitmentTreeError::InvalidData(format!("max_leaf_position: {e}")))?;
Ok(match pos {
Some(p) => p + 1,
None => Position::from(0),
})
}
}
Loading
Loading