From 4b690e6ca8c809693b4119c17500ac724494cd87 Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 10 Apr 2026 13:09:13 -0600 Subject: [PATCH] Expose commitment-tree primitives instead of monolithic witness generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach bundled witness generation inside zcash_client_sqlite, but the voting crate needs full control over the ephemeral tree lifecycle (shard copying, frontier insertion, checkpoint management) to handle error recovery and progress reporting at the FFI boundary. Replace the monolithic `generate_orchard_witnesses_at_frontier` method with three focused primitives that let the caller compose its own witness pipeline: - `WalletDb::conn()` — access to the underlying connection for shard data queries - `create_orchard_tree_tables(conn)` — stand up the Orchard tree schema in any connection (e.g. in-memory), reusing the canonical DDL - `SqliteShardStore::from_connection` made `pub` — construct a shard store over any connection that has the right schema Made-with: Cursor --- zcash_client_sqlite/CHANGELOG.md | 8 +- zcash_client_sqlite/src/lib.rs | 40 +-- .../src/wallet/commitment_tree.rs | 289 +----------------- 3 files changed, 29 insertions(+), 308 deletions(-) diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index 934a3d2db5..f8a5021d1f 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -13,8 +13,12 @@ workspace. ### Added - `WalletDb::get_orchard_notes_at_snapshot` returns Orchard notes received and unspent as of a given height, for governance voting snapshots. -- `WalletDb::generate_orchard_witnesses_at_frontier` generates Merkle - witnesses at a historical frontier using an ephemeral in-memory tree. +- `WalletDb::conn()` provides access to the underlying connection handle. +- `wallet::commitment_tree::create_orchard_tree_tables` creates the + Orchard commitment-tree schema in a given connection, enabling + construction of an ephemeral in-memory `SqliteShardStore`. +- `wallet::commitment_tree::SqliteShardStore::from_connection` is now + publicly accessible. ## [0.19.5] - 2026-03-10 diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index fd4223058b..29ffbcf461 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -401,6 +401,11 @@ impl Borrow for SqlTransaction<'_> { } impl WalletDb { + /// Returns a reference to the underlying connection handle. + pub fn conn(&self) -> &C { + &self.conn + } + /// Returns the network parameters that this walletdb instance is bound to. pub fn params(&self) -> &P { &self.params @@ -2313,8 +2318,8 @@ impl WalletCommitmentTrees // --- Governance-specific methods --- // // These methods support Zcash shielded voting and are not part of the -// general-purpose wallet traits. They provide backward-looking queries -// (historical snapshot) and witness generation at an external frontier. +// general-purpose wallet traits. They provide a backward-looking snapshot +// query for historical note selection. #[cfg(feature = "orchard")] impl, P: consensus::Parameters, CL, R> WalletDb { @@ -2337,37 +2342,6 @@ impl, P: consensus::Parameters, CL, R> WalletDb< snapshot_height, ) } - - /// Generate Orchard Merkle witnesses at a historical frontier. - /// - /// Copies the wallet's Orchard shard data into an ephemeral in-memory - /// database, inserts the provided frontier as a checkpoint, and generates - /// a witness for each of the given note positions. - /// - /// The wallet DB is strictly read-only — shard data is copied, not modified. - pub fn generate_orchard_witnesses_at_frontier( - &self, - note_positions: &[Position], - frontier: incrementalmerkletree::frontier::NonEmptyFrontier< - orchard::tree::MerkleHashOrchard, - >, - checkpoint_height: BlockHeight, - ) -> Result< - Vec< - incrementalmerkletree::MerklePath< - orchard::tree::MerkleHashOrchard, - { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, - >, - >, - SqliteClientError, - > { - wallet::commitment_tree::generate_orchard_witnesses_at_frontier( - self.conn.borrow(), - note_positions, - frontier, - checkpoint_height, - ) - } } /// A handle for the SQLite block source. diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 983a0c618d..b824e20426 100644 --- a/zcash_client_sqlite/src/wallet/commitment_tree.rs +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -103,10 +103,7 @@ pub struct SqliteShardStore { impl SqliteShardStore { const SHARD_ROOT_LEVEL: Level = Level::new(SHARD_HEIGHT); - pub(crate) fn from_connection( - conn: C, - table_prefix: &'static str, - ) -> Result { + pub fn from_connection(conn: C, table_prefix: &'static str) -> Result { Ok(SqliteShardStore { conn, table_prefix, @@ -1140,179 +1137,23 @@ pub(crate) fn check_witnesses( Ok(scan_ranges) } -/// Generate Orchard Merkle witnesses at a historical frontier. +/// Creates the Orchard commitment-tree tables in the given connection. /// -/// Copies the wallet's Orchard shard data into an ephemeral in-memory database, -/// inserts the provided frontier (from lightwalletd) as a checkpoint, and -/// generates a witness for each of the given note positions. -/// -/// The wallet DB is strictly read-only — shard data is copied, not modified. -/// -/// This is used by governance voting: the wallet tree may have advanced past -/// the snapshot height, but we need witnesses anchored at the snapshot frontier. +/// This enables constructing an ephemeral in-memory [`SqliteShardStore`] for +/// operations like witness generation at a historical frontier, without +/// requiring the full wallet schema. #[cfg(feature = "orchard")] -pub fn generate_orchard_witnesses_at_frontier( - conn: &rusqlite::Connection, - note_positions: &[Position], - frontier: incrementalmerkletree::frontier::NonEmptyFrontier, - checkpoint_height: BlockHeight, -) -> Result< - Vec< - incrementalmerkletree::MerklePath< - orchard::tree::MerkleHashOrchard, - { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, - >, - >, - SqliteClientError, -> { - use incrementalmerkletree::Marking; - use shardtree::ShardTree; - use shardtree::store::Checkpoint; - use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; - - let frontier_position = frontier.position(); - - // Create in-memory DB with the tree schema - let mem_conn = rusqlite::Connection::open_in_memory().map_err(SqliteClientError::DbError)?; - - mem_conn - .execute_batch( - "CREATE TABLE orchard_tree_shards ( - shard_index INTEGER PRIMARY KEY, - subtree_end_height INTEGER, - root_hash BLOB, - shard_data BLOB, - contains_marked INTEGER, - CONSTRAINT root_unique UNIQUE (root_hash) - ); - CREATE TABLE orchard_tree_cap ( - cap_id INTEGER PRIMARY KEY, - cap_data BLOB NOT NULL - ); - CREATE TABLE orchard_tree_checkpoints ( - checkpoint_id INTEGER PRIMARY KEY, - position INTEGER - ); - CREATE TABLE orchard_tree_checkpoint_marks_removed ( - checkpoint_id INTEGER NOT NULL, - mark_removed_position INTEGER NOT NULL, - FOREIGN KEY (checkpoint_id) REFERENCES orchard_tree_checkpoints(checkpoint_id) - ON DELETE CASCADE, - CONSTRAINT spend_position_unique UNIQUE (checkpoint_id, mark_removed_position) - );", - ) - .map_err(SqliteClientError::DbError)?; - - // Copy shard data from wallet into in-memory DB - { - use rusqlite::types::Value; - - let mut stmt = conn - .prepare( - "SELECT shard_index, subtree_end_height, root_hash, shard_data, contains_marked - FROM orchard_tree_shards", - ) - .map_err(SqliteClientError::DbError)?; - let mut rows = stmt.query([]).map_err(SqliteClientError::DbError)?; - while let Some(row) = rows.next().map_err(SqliteClientError::DbError)? { - mem_conn - .execute( - "INSERT INTO orchard_tree_shards - (shard_index, subtree_end_height, root_hash, shard_data, contains_marked) - VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![ - row.get::<_, Value>(0)?, - row.get::<_, Value>(1)?, - row.get::<_, Value>(2)?, - row.get::<_, Value>(3)?, - row.get::<_, Value>(4)?, - ], - ) - .map_err(SqliteClientError::DbError)?; - } - } - - // Copy cap data - { - use rusqlite::types::Value; - - let mut stmt = conn - .prepare("SELECT cap_id, cap_data FROM orchard_tree_cap") - .map_err(SqliteClientError::DbError)?; - let mut rows = stmt.query([]).map_err(SqliteClientError::DbError)?; - while let Some(row) = rows.next().map_err(SqliteClientError::DbError)? { - mem_conn - .execute( - "INSERT INTO orchard_tree_cap (cap_id, cap_data) VALUES (?1, ?2)", - rusqlite::params![row.get::<_, Value>(0)?, row.get::<_, Value>(1)?,], - ) - .map_err(SqliteClientError::DbError)?; - } - } - - // Build ShardTree from in-memory store - let tx = mem_conn - .unchecked_transaction() - .map_err(SqliteClientError::DbError)?; - - let store = SqliteShardStore::< - _, - orchard::tree::MerkleHashOrchard, - ORCHARD_SHARD_HEIGHT, - >::from_connection(&tx, "orchard") - .map_err(SqliteClientError::DbError)?; - - let mut tree = - ShardTree::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>::new( - store, 100, - ); - - // Insert frontier + checkpoint - tree.insert_frontier_nodes( - frontier, - Retention::Checkpoint { - id: checkpoint_height, - marking: Marking::None, - }, - ) - .map_err(|e| { - SqliteClientError::CorruptedData(format!("failed to insert frontier nodes: {}", e)) - })?; - - tree.store_mut() - .add_checkpoint( - checkpoint_height, - Checkpoint::at_position(frontier_position), - ) - .map_err(|e| { - SqliteClientError::CorruptedData(format!("failed to add checkpoint: {}", e)) - })?; - - // Generate witness per note position - let mut witnesses = Vec::with_capacity(note_positions.len()); - for &pos in note_positions { - let merkle_path = tree - .witness_at_checkpoint_id(pos, &checkpoint_height) - .map_err(|e| { - SqliteClientError::CorruptedData(format!( - "failed to generate witness for position {}: {} \ - (wallet may need to sync through snapshot height)", - u64::from(pos), - e - )) - })? - .ok_or_else(|| { - SqliteClientError::CorruptedData(format!( - "no witness available for position {} \ - (wallet missing shard data — sync through snapshot height)", - u64::from(pos) - )) - })?; - - witnesses.push(merkle_path); - } - - Ok(witnesses) +pub fn create_orchard_tree_tables(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> { + use super::db::{ + TABLE_ORCHARD_TREE_CAP, TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED, + TABLE_ORCHARD_TREE_CHECKPOINTS, TABLE_ORCHARD_TREE_SHARDS, + }; + conn.execute_batch(&format!( + "{TABLE_ORCHARD_TREE_SHARDS};\ + {TABLE_ORCHARD_TREE_CAP};\ + {TABLE_ORCHARD_TREE_CHECKPOINTS};\ + {TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED};", + )) } #[cfg(test)] @@ -1401,11 +1242,6 @@ mod tests { super::check_rewind_remove_mark(new_tree::); } - #[test] - fn witnesses_at_frontier() { - super::witnesses_at_frontier() - } - #[test] fn put_shard_roots() { super::put_shard_roots::() @@ -1522,97 +1358,4 @@ mod tests { ] ); } - - /// Test that `generate_orchard_witnesses_at_frontier` produces valid - /// witnesses when given a frontier extracted from an earlier tree state. - #[cfg(feature = "orchard")] - fn witnesses_at_frontier() { - use ::orchard::tree::MerkleHashOrchard; - use incrementalmerkletree::frontier::Frontier; - use rand::SeedableRng; - use rand_chacha::ChaChaRng; - use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; - - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path( - data_file.path(), - Network::TestNetwork, - test_clock(), - test_rng(), - ) - .unwrap(); - data_file.keep().unwrap(); - - WalletMigrator::new().init_or_migrate(&mut db_data).unwrap(); - - let mut rng = ChaChaRng::seed_from_u64(0); - - // Build a tree with some leaves, marking one as our note. - // We track the frontier separately to capture the tree state at a - // snapshot height, while the wallet DB holds the persistent shard data. - let mut frontier_tree: Frontier = Frontier::empty(); - let snapshot_height = BlockHeight::from(100); - let note_position; - let note_leaf; - - { - let tx = db_data.conn.transaction().unwrap(); - let store = - SqliteShardStore::<_, MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>::from_connection( - &tx, "orchard", - ) - .unwrap(); - let mut tree = ShardTree::< - _, - { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, - ORCHARD_SHARD_HEIGHT, - >::new(store, 100); - - // Insert 5 leaves. Mark leaf 2 as our note. - let mut saved_leaf = None; - for i in 0u64..5 { - let leaf = MerkleHashOrchard::random(&mut rng); - let retention = if i == 2 { - saved_leaf = Some(leaf); - Retention::Marked - } else { - Retention::Ephemeral - }; - tree.append(leaf, retention).unwrap(); - frontier_tree.append(leaf); - } - note_position = Position::from(2); - note_leaf = saved_leaf.unwrap(); - - // Advance the tree past the snapshot height, simulating the - // wallet continuing to sync after the voting snapshot. - tree.checkpoint(snapshot_height).unwrap(); - for _ in 0..5 { - let leaf = MerkleHashOrchard::random(&mut rng); - tree.append(leaf, Retention::Ephemeral).unwrap(); - } - tree.checkpoint(BlockHeight::from(200)).unwrap(); - - tx.commit().unwrap(); - } - - // The frontier_tree captured the state after 5 leaves. - let expected_root = frontier_tree.root(); - let frontier = frontier_tree.take().expect("frontier is non-empty"); - - // Generate witness using the function under test - let witnesses = super::generate_orchard_witnesses_at_frontier( - &db_data.conn, - &[note_position], - frontier, - snapshot_height, - ) - .expect("witness generation should succeed"); - - assert_eq!(witnesses.len(), 1); - - // The witness, combined with the note's leaf hash, must recompute - // to the tree root at the snapshot height. - assert_eq!(witnesses[0].root(note_leaf), expected_root); - } }