Skip to content

Expose commitment-tree primitives instead of monolithic witness generation#23

Merged
p0mvn merged 1 commit into
shielded-voting-wallet-supportfrom
expose-tree-primitives-for-voting
Apr 10, 2026
Merged

Expose commitment-tree primitives instead of monolithic witness generation#23
p0mvn merged 1 commit into
shielded-voting-wallet-supportfrom
expose-tree-primitives-for-voting

Conversation

@p0mvn
Copy link
Copy Markdown

@p0mvn p0mvn commented Apr 10, 2026

Summary

  • Replace WalletDb::generate_orchard_witnesses_at_frontier with three focused building blocks: WalletDb::conn(), create_orchard_tree_tables(), and a public SqliteShardStore::from_connection
  • The voting crate (librustvoting) 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
  • Monolithic helper removed along with its test; CHANGELOG updated to reflect the new public API surface

Comment on lines 1148 to +1149
#[cfg(feature = "orchard")]
pub fn generate_orchard_witnesses_at_frontier(
conn: &rusqlite::Connection,
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,
> {
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> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: new public function #1

Comment on lines +404 to +407
/// Returns a reference to the underlying connection handle.
pub fn conn(&self) -> &C {
&self.conn
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: new public method #2

const SHARD_ROOT_LEVEL: Level = Level::new(SHARD_HEIGHT);

pub(crate) fn from_connection(
pub fn from_connection(
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: public method function #3

…ation

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
@p0mvn p0mvn merged commit 6858db4 into shielded-voting-wallet-support Apr 10, 2026
28 of 45 checks passed
p0mvn added a commit that referenced this pull request Apr 11, 2026
…ss generation"

This reverts merge commit 6858db4 (PR #23).

The approach of exposing individual commitment-tree primitives
(conn(), create_orchard_tree_tables, pub from_connection) is being
reconsidered. Revert to restore the previous API surface on the
shielded-voting-wallet-support branch.

Made-with: Cursor
greg0x pushed a commit that referenced this pull request Apr 14, 2026
…f0768ea4..dd0ea2c3c5

dd0ea2c3c5 Merge pull request #23 from zcash/2026-03-doc-fixes
93b26b7db1 v0.4.1 release - minor doc fixups
d528fa82e3 Merge pull request #22 from zcash/2025-12-doc-git-subtree
b421ef0b34 Merge pull request #22 from zcash/2025-12-doc-git-subtree
4a26e54c36 Fix git subtree command syntax in README
34bb38b606 Merge pull request #20 from zcash/doc/get_block
a507182f92 Document GetBlock transparent data behavior and clarify nullifier RPCs
fc5cc9a4b1 Merge pull request #19 from zcash/deprecate_get_nullifiers
f0ebc72cad Mark `GetBlockNullifiers` and `GetBlockRangeNullifiers` as deprecated.
9a5f7a0eec Merge pull request #16 from zcash/2026-01-remove-fullHeader
f1095897d9 revert the previous PR that added BlockID.fullHeader
99d9bf9fff Merge pull request #15 from zcash/2025-12-compactblock-doc
bbdd689d73 either CompactBlock.{hash, prevHash}, or CompactBlock.header
aa42926ed3 Merge pull request #13 from pacu/add-README
da4d3d57d8 Add rationale suggested by @LarryRuane
4edd22465b fix type
e36df314b8 clean up whitespace and line width
215cb5a3f3 Add PR suggestions. Organize Clients section
3e78d5d872 Apply suggestions from code review
96b86544db Apply suggestion from @LarryRuane
6259b97df0 Apply suggestion from @nuttycom
f2fcef3e42 Apply suggestion from @nuttycom
8cc1e66b11 Apply suggestion from @LarryRuane
99132222dd Apply suggestion from @nuttycom
207cc5e6f9 create a README file with context and instructions
7392b9706f Merge pull request #12 from zcash/release/v0.4.0

git-subtree-dir: zcash_client_backend/lightwallet-protocol
git-subtree-split: dd0ea2c3c5827a433e62c2f936b89efa2dec5a9a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants