Problem
ClientPersistentCommitmentTree in grovedb-commitment-tree has a hard dependency on rusqlite and manages its own SQLite tables with hardcoded names (commitment_tree_shards, commitment_tree_cap, commitment_tree_checkpoints, commitment_tree_checkpoint_marks_removed).
This creates several problems for consumers:
1. No multi-instance support
The hardcoded table names mean only one commitment tree can exist per SQLite database. Applications that need multiple trees (e.g., a wallet app managing multiple wallets, each with its own shielded state) must either:
- Use separate SQLite files per instance (fragmented storage, complicates backups and migrations)
- Modify the SDK to add table prefixes or scoping columns
2. Storage technology lock-in
A library should not dictate storage implementation. Consumers may need:
- Distributed architecture — database server on a different host
- Custom backup strategies — integrated with application-level backup/restore
- Alternative storage backends — embedded KV stores, cloud-native databases, in-memory stores for testing
- Schema migration control — the library creates tables autonomously via
CREATE TABLE IF NOT EXISTS, which conflicts with applications that manage their own migration pipelines
3. Dependency bloat
Pulling rusqlite (with bundled feature compiling SQLite from C source) into a cryptographic tree library is a heavy dependency for what is essentially a key-value persistence layer.
Proposed Solution
Abstract storage behind a trait:
pub trait CommitmentTreeStore {
type Error: std::error::Error;
fn get_shard(&self, index: u64) -> Result<Option<Vec<u8>>, Self::Error>;
fn put_shard(&mut self, index: u64, data: &[u8]) -> Result<(), Self::Error>;
fn get_cap(&self) -> Result<Option<Vec<u8>>, Self::Error>;
fn put_cap(&mut self, data: &[u8]) -> Result<(), Self::Error>;
fn add_checkpoint(&mut self, id: u64, position: Option<u64>) -> Result<(), Self::Error>;
// ... etc for all current sql_helpers operations
}
Then provide rusqlite support as an optional feature (e.g., feature = "sqlite") with a default SqliteCommitmentTreeStore implementation, rather than baking it into the core type.
This would let consumers:
- Implement the trait on their own database connection (with their own table names, scoping, migrations)
- Use non-SQLite backends
- Control schema creation and versioning
- Support multiple tree instances in a single database
Current Workaround
In dash-evo-tool, we work around the single-tree limitation by opening a separate SQLite file per wallet (<data_dir>/shielded/<wallet_hash>.db). This works but fragments the database, complicates backups, and bypasses the app's migration system.
Context
Discovered while fixing a critical multi-wallet bug where all wallets shared the same commitment tree tables, causing anchor mismatches and invalid Merkle witnesses: dashpay/dash-evo-tool#786.
🤖 Co-authored by Claudius the Magnificent AI Agent
Problem
ClientPersistentCommitmentTreeingrovedb-commitment-treehas a hard dependency onrusqliteand manages its own SQLite tables with hardcoded names (commitment_tree_shards,commitment_tree_cap,commitment_tree_checkpoints,commitment_tree_checkpoint_marks_removed).This creates several problems for consumers:
1. No multi-instance support
The hardcoded table names mean only one commitment tree can exist per SQLite database. Applications that need multiple trees (e.g., a wallet app managing multiple wallets, each with its own shielded state) must either:
2. Storage technology lock-in
A library should not dictate storage implementation. Consumers may need:
CREATE TABLE IF NOT EXISTS, which conflicts with applications that manage their own migration pipelines3. Dependency bloat
Pulling
rusqlite(withbundledfeature compiling SQLite from C source) into a cryptographic tree library is a heavy dependency for what is essentially a key-value persistence layer.Proposed Solution
Abstract storage behind a trait:
Then provide
rusqlitesupport as an optional feature (e.g.,feature = "sqlite") with a defaultSqliteCommitmentTreeStoreimplementation, rather than baking it into the core type.This would let consumers:
Current Workaround
In dash-evo-tool, we work around the single-tree limitation by opening a separate SQLite file per wallet (
<data_dir>/shielded/<wallet_hash>.db). This works but fragments the database, complicates backups, and bypasses the app's migration system.Context
Discovered while fixing a critical multi-wallet bug where all wallets shared the same commitment tree tables, causing anchor mismatches and invalid Merkle witnesses: dashpay/dash-evo-tool#786.
🤖 Co-authored by Claudius the Magnificent AI Agent