Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
219ba2e
feat: orchard zk
QuantumExplorer Feb 9, 2026
8957d80
fixes
QuantumExplorer Feb 10, 2026
44eaab5
clippy
QuantumExplorer Feb 10, 2026
164f375
more work
QuantumExplorer Feb 12, 2026
757a2be
more work
QuantumExplorer Feb 12, 2026
1c229f7
more work
QuantumExplorer Feb 12, 2026
4a05f07
more work
QuantumExplorer Feb 12, 2026
3c68b0c
more work
QuantumExplorer Feb 12, 2026
9b01260
fixes
QuantumExplorer Feb 12, 2026
3b89abc
fixes
QuantumExplorer Feb 12, 2026
da5fbc7
fixes
QuantumExplorer Feb 12, 2026
266b5e9
more work
QuantumExplorer Feb 12, 2026
c7f098a
more work
QuantumExplorer Feb 14, 2026
761344c
new trees
QuantumExplorer Feb 14, 2026
1db922b
more work
QuantumExplorer Feb 14, 2026
d659ed2
more work
QuantumExplorer Feb 15, 2026
26b2f30
more work
QuantumExplorer Feb 16, 2026
bf11591
more work
QuantumExplorer Feb 17, 2026
0f56726
more work
QuantumExplorer Feb 17, 2026
f7b295c
more work
QuantumExplorer Feb 17, 2026
8a88520
more work
QuantumExplorer Feb 17, 2026
66ea6e2
more work
QuantumExplorer Feb 18, 2026
d4d22af
more work
QuantumExplorer Feb 18, 2026
d5bec5b
more work
QuantumExplorer Feb 18, 2026
814331f
more work
QuantumExplorer Feb 18, 2026
2645f87
more work
QuantumExplorer Feb 18, 2026
a489864
more work
QuantumExplorer Feb 18, 2026
fcaab54
more work
QuantumExplorer Feb 18, 2026
dc8c250
more work
QuantumExplorer Feb 18, 2026
e2b960b
better dense fixed size tree
QuantumExplorer Feb 19, 2026
20e2b47
better dense fixed size tree
QuantumExplorer Feb 19, 2026
08a2d9e
more work
QuantumExplorer Feb 20, 2026
6ec4165
more work
QuantumExplorer Feb 22, 2026
e42425f
more work
QuantumExplorer Feb 22, 2026
e7de0cf
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 22, 2026
752c731
more work
QuantumExplorer Feb 22, 2026
72ec9d9
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 22, 2026
8841052
more work
QuantumExplorer Feb 22, 2026
4d778c6
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 23, 2026
679ea5a
fmt
QuantumExplorer Feb 23, 2026
56120fb
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 24, 2026
59c25a7
fmt
QuantumExplorer Feb 24, 2026
0df4c6b
test: add checkpoint ID uniqueness tests for commitment tree
QuantumExplorer Feb 25, 2026
ca972f7
more work
QuantumExplorer Feb 26, 2026
affb75f
more work
QuantumExplorer Feb 26, 2026
22a26d9
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 26, 2026
e1e3aab
refactor: split bloated GroveOp variants into dedicated non-Merk tree…
QuantumExplorer Feb 26, 2026
6315a60
more work
QuantumExplorer Feb 27, 2026
63e0f06
more work
QuantumExplorer Feb 27, 2026
6483e12
Merge branch 'develop' into feat/orchardZK
QuantumExplorer Feb 27, 2026
35c5fe6
test: add audit-driven tests for non-Merk tree types and proof genera…
QuantumExplorer Feb 27, 2026
9020fb2
fix: V1 proof SumItem/ItemWithSumItem + audit-driven quality fixes an…
QuantumExplorer Feb 27, 2026
9ce377f
more work
QuantumExplorer Feb 27, 2026
6f631e8
more work
QuantumExplorer Feb 27, 2026
3595fbe
more work
QuantumExplorer Feb 27, 2026
91accc8
more work
QuantumExplorer Feb 27, 2026
86b108d
more work
QuantumExplorer Feb 27, 2026
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
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ members = [
"path",
"grovedbg-types",
"grovedb-version",
"grovedb-epoch-based-storage-flags"
, "grovedb-element"]
"grovedb-epoch-based-storage-flags",
"grovedb-element",
"grovedb-commitment-tree",
]
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
22 changes: 22 additions & 0 deletions grovedb-commitment-tree/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[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 = []
client = ["shardtree"]

[dependencies]
orchard = { version = "0.12", features = ["circuit"] }
incrementalmerkletree = "0.8"
shardtree = { version = "0.6", optional = true }
thiserror = "2.0"
210 changes: 210 additions & 0 deletions grovedb-commitment-tree/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//! Client-side commitment tree with full witness generation.
//!
//! This module provides [`ClientCommitmentTree`], a wrapper around
//! `shardtree::ShardTree` with an in-memory store, pinned to Orchard types.
//! It is intended for wallets and test harnesses that need to generate
//! Merkle path witnesses for spending notes.
//!
//! Enable the `client` feature to use this module:
//! ```toml
//! grovedb-commitment-tree = { version = "4", features = ["client"] }
//! ```

use incrementalmerkletree::{Position, Retention};
use orchard::{
tree::{Anchor, MerkleHashOrchard, MerklePath},
NOTE_COMMITMENT_TREE_DEPTH,
};
use shardtree::{store::memory::MemoryShardStore, ShardTree};

use crate::{merkle_hash_from_bytes, CommitmentTreeError};

/// Shard height for the ShardTree. Each shard covers 16 levels.
const SHARD_HEIGHT: u8 = 4;

/// 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::CommitmentFrontier) instead.
pub struct ClientCommitmentTree {
inner: ShardTree<
MemoryShardStore<MerkleHashOrchard, u32>,
{ NOTE_COMMITMENT_TREE_DEPTH as u8 },
SHARD_HEIGHT,
>,
}

impl ClientCommitmentTree {
/// 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),
})
}
}

#[cfg(test)]
mod tests {
use incrementalmerkletree::{Hashable, Level, Retention};

use super::*;

fn test_leaf(index: u64) -> [u8; 32] {
let empty = MerkleHashOrchard::empty_leaf();
let varied =
MerkleHashOrchard::combine(Level::from((index % 31) as u8 + 1), &empty, &empty);
MerkleHashOrchard::combine(Level::from(0), &empty, &varied).to_bytes()
}

#[test]
fn test_empty_tree() {
let tree = ClientCommitmentTree::new(10);
assert_eq!(tree.max_leaf_position().unwrap(), None);
assert_eq!(tree.anchor().unwrap(), Anchor::empty_tree());
}

#[test]
fn test_append_and_position() {
let mut tree = ClientCommitmentTree::new(10);

tree.append(test_leaf(0), Retention::Marked).unwrap();
assert_eq!(tree.max_leaf_position().unwrap(), Some(Position::from(0)));

tree.append(test_leaf(1), Retention::Ephemeral).unwrap();
assert_eq!(tree.max_leaf_position().unwrap(), Some(Position::from(1)));
}

#[test]
fn test_anchor_changes() {
let mut tree = ClientCommitmentTree::new(10);
let empty_anchor = tree.anchor().unwrap();

tree.append(test_leaf(0), Retention::Marked).unwrap();
let anchor1 = tree.anchor().unwrap();
assert_ne!(empty_anchor, anchor1);

tree.append(test_leaf(1), Retention::Marked).unwrap();
let anchor2 = tree.anchor().unwrap();
assert_ne!(anchor1, anchor2);
}

#[test]
fn test_witness_generation() {
let mut tree = ClientCommitmentTree::new(10);

// Append a marked leaf so we can witness it
tree.append(test_leaf(0), Retention::Marked).unwrap();
tree.append(test_leaf(1), Retention::Ephemeral).unwrap();
tree.checkpoint(1).unwrap();

// Witness for position 0 at current state
let path = tree.witness(Position::from(0), 0).unwrap();
assert!(path.is_some(), "should produce witness for marked leaf");
}

#[test]
#[cfg(feature = "server")]
fn test_frontier_and_client_same_root() {
use crate::CommitmentFrontier;

let mut frontier = CommitmentFrontier::new();
let mut client = ClientCommitmentTree::new(10);

for i in 0..20u64 {
frontier.append(test_leaf(i)).unwrap();
client.append(test_leaf(i), Retention::Ephemeral).unwrap();
}

assert_eq!(frontier.anchor(), client.anchor().unwrap());
}
}
Loading
Loading