diff --git a/pczt/src/orchard.rs b/pczt/src/orchard.rs index ebe4cd7c73..0a9cc8fa5b 100644 --- a/pczt/src/orchard.rs +++ b/pczt/src/orchard.rs @@ -111,6 +111,7 @@ pub struct Spend { /// /// This is set by the Signer. #[serde_as(as = "Option<[_; 64]>")] + #[getset(get = "pub")] pub(crate) spend_auth_sig: Option<[u8; 64]>, /// The [raw encoding] of the Orchard payment address that received the note being spent. diff --git a/pczt/src/roles/signer/mod.rs b/pczt/src/roles/signer/mod.rs index 7fcd603ec3..98188f132c 100644 --- a/pczt/src/roles/signer/mod.rs +++ b/pczt/src/roles/signer/mod.rs @@ -95,6 +95,11 @@ impl Signer { }) } + /// Returns the cached shielded (Orchard + Sapling) sighash. + pub fn shielded_sighash(&self) -> [u8; 32] { + self.shielded_sighash + } + /// Signs the transparent spend at the given index with the given spending key. /// /// It is the caller's responsibility to perform any semantic validity checks on the diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 7798bd1df2..fd4223058b 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -2310,6 +2310,66 @@ 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. + +#[cfg(feature = "orchard")] +impl, P: consensus::Parameters, CL, R> WalletDb { + /// Return all Orchard notes received at or before `snapshot_height` and + /// unspent as of that height, for the given account. + /// + /// Unlike [`InputSource::select_unspent_notes`] which is forward-looking + /// (based on tx expiry), this is backward-looking: a note is included if + /// it was mined at or before `snapshot_height` and no spend of that note + /// was mined at or before `snapshot_height`. + pub fn get_orchard_notes_at_snapshot( + &self, + account: AccountUuid, + snapshot_height: BlockHeight, + ) -> Result>, SqliteClientError> { + wallet::orchard::get_orchard_notes_at_snapshot( + self.conn.borrow(), + &self.params, + account, + 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. pub struct BlockDb(Connection); diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 6f4104c964..b650137815 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,6 +1137,161 @@ pub(crate) fn check_witnesses( Ok(scan_ranges) } +/// 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 (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. +#[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 and copy orchard tree tables + 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)?; + + // Attach wallet DB read-only and copy shard data + let wallet_path = conn.path().ok_or_else(|| { + SqliteClientError::CorruptedData("cannot determine wallet DB path".to_string()) + })?; + + mem_conn + .execute("ATTACH DATABASE ?1 AS wallet", [wallet_path]) + .map_err(SqliteClientError::DbError)?; + + for table in &[ + "orchard_tree_shards", + "orchard_tree_cap", + "orchard_tree_checkpoints", + "orchard_tree_checkpoint_marks_removed", + ] { + mem_conn + .execute( + &format!("INSERT INTO main.{t} SELECT * FROM wallet.{t}", t = table), + [], + ) + .map_err(SqliteClientError::DbError)?; + } + + mem_conn + .execute("DETACH DATABASE wallet", []) + .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) +} + #[cfg(test)] mod tests { use tempfile::NamedTempFile; diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 664e9294ec..6a77ba7816 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -244,6 +244,55 @@ pub(crate) fn select_spendable_orchard_notes( ) } +/// Return all Orchard notes that were received at or before `snapshot_height` +/// and unspent as of `snapshot_height`, for the given account. +/// +/// This is a backward-looking query used for governance voting snapshots, +/// unlike `select_spendable_notes` which is forward-looking (via tx expiry). +pub fn get_orchard_notes_at_snapshot( + conn: &Connection, + params: &P, + account: AccountUuid, + snapshot_height: BlockHeight, +) -> Result>, SqliteClientError> { + let mut stmt = conn.prepare_cached( + "SELECT + rn.id AS id, t.txid, rn.action_index, + rn.diversifier, rn.value, rn.rho, rn.rseed, rn.commitment_tree_position, + accounts.ufvk AS ufvk, rn.recipient_key_scope, + t.block AS mined_height, + NULL AS max_shielding_input_height + FROM orchard_received_notes rn + INNER JOIN accounts ON accounts.id = rn.account_id + INNER JOIN transactions t ON t.id_tx = rn.transaction_id + WHERE accounts.uuid = :account_uuid + AND t.block IS NOT NULL + AND t.block <= :snapshot_height + AND rn.nf IS NOT NULL + AND rn.commitment_tree_position IS NOT NULL + AND rn.recipient_key_scope IN (0, 1) + AND accounts.ufvk IS NOT NULL + AND rn.id NOT IN ( + SELECT rns.orchard_received_note_id + FROM orchard_received_note_spends rns + JOIN transactions t_spend ON t_spend.id_tx = rns.transaction_id + WHERE t_spend.block IS NOT NULL + AND t_spend.block <= :snapshot_height + ) + ORDER BY rn.commitment_tree_position", + )?; + + let rows = stmt.query_and_then( + named_params![ + ":account_uuid": account.0, + ":snapshot_height": u32::from(snapshot_height), + ], + |row| to_received_note(params, row), + )?; + + rows.filter_map(|r| r.transpose()).collect() +} + pub(crate) fn ensure_address< T: ReceivedOrchardOutput, P: consensus::Parameters,