Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pczt/src/orchard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions pczt/src/roles/signer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2310,6 +2310,66 @@ impl<P: consensus::Parameters, CL, R> 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<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletDb<C, P, CL, R> {
/// 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<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, 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);

Expand Down
160 changes: 156 additions & 4 deletions zcash_client_sqlite/src/wallet/commitment_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,7 @@ pub struct SqliteShardStore<C, H, const SHARD_HEIGHT: u8> {
impl<C, H, const SHARD_HEIGHT: u8> SqliteShardStore<C, H, SHARD_HEIGHT> {
const SHARD_ROOT_LEVEL: Level = Level::new(SHARD_HEIGHT);

pub(crate) fn from_connection(
conn: C,
table_prefix: &'static str,
) -> Result<Self, rusqlite::Error> {
pub fn from_connection(conn: C, table_prefix: &'static str) -> Result<Self, rusqlite::Error> {
Ok(SqliteShardStore {
conn,
table_prefix,
Expand Down Expand Up @@ -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<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 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;
Expand Down
49 changes: 49 additions & 0 deletions zcash_client_sqlite/src/wallet/orchard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,55 @@ pub(crate) fn select_spendable_orchard_notes<P: consensus::Parameters>(
)
}

/// 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<P: consensus::Parameters>(
conn: &Connection,
params: &P,
account: AccountUuid,
snapshot_height: BlockHeight,
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, 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<AccountId = AccountUuid>,
P: consensus::Parameters,
Expand Down
Loading