-
Notifications
You must be signed in to change notification settings - Fork 26
feat: add grovedb-commitment-tree crate #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
595b30c
feat: add grovedb-commitment-tree crate
QuantumExplorer f922f28
fix: address audit findings and add sinsemilla_hash_calls to Operatio…
QuantumExplorer 5d227a1
more work
QuantumExplorer a81261a
fix: eliminate test_leaf collision and reject invalid annotation flags
QuantumExplorer de33d13
chore: fix clippy warnings and enforce missing_docs lint
QuantumExplorer 29c51f3
more work
QuantumExplorer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
| thiserror = "2.0" | ||
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| criterion = "0.4" | ||
| rand = "0.8" | ||
|
|
||
| [[bench]] | ||
| name = "verification" | ||
| harness = false | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
121
grovedb-commitment-tree/src/client/client_memory_commitment_tree.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: dashpay/grovedb
Length of output: 338
🏁 Script executed:
Repository: dashpay/grovedb
Length of output: 5780
🏁 Script executed:
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, butshardtree = { version = "0.6", optional = true }requiresincrementalmerkletree >= 0.8.1. Change this toincrementalmerkletree = "0.8.1"to ensure the correct version is selected and avoid potential dependency resolution issues.🤖 Prompt for AI Agents