Skip to content

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

Merged
p0mvn merged 1 commit intoshielded-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 intoshielded-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
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