Skip to content

ClientPersistentCommitmentTree: abstract storage instead of depending on rusqlite directly #653

@lklimek

Description

@lklimek

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions