zcash_client_sqlite: Add receiving key scope information to received notes.

This commit is contained in:
Kris Nuttycombe 2023-11-28 17:03:13 -07:00
parent 8c1480304e
commit 4a7dd2bed2
4 changed files with 135 additions and 11 deletions

View File

@ -75,6 +75,7 @@ use std::ops::RangeInclusive;
use tracing::debug;
use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary};
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
use zcash_primitives::zip32::Scope;
use zcash_client_backend::data_api::{
scanning::{ScanPriority, ScanRange},
@ -137,6 +138,13 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
}
}
pub(crate) fn scope_code(scope: Scope) -> i64 {
match scope {
Scope::External => 0i64,
Scope::Internal => 1i64,
}
}
pub(crate) fn memo_repr(memo: Option<&MemoBytes>) -> Option<&[u8]> {
memo.map(|m| {
if m == &MemoBytes::empty() {

View File

@ -248,6 +248,7 @@ mod tests {
memo BLOB,
spent INTEGER,
commitment_tree_position INTEGER,
recipient_key_scope INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (account) REFERENCES accounts(account),
FOREIGN KEY (spent) REFERENCES transactions(id_tx),

View File

@ -5,6 +5,7 @@ mod addresses_table;
mod initial_setup;
mod nullifier_map;
mod received_notes_nullable_nf;
mod receiving_key_scopes;
mod sapling_memo_consistency;
mod sent_notes_to_internal;
mod shardtree_support;
@ -40,17 +41,17 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
// |
// v_transactions_net
// |
// received_notes_nullable_nf
// / | \
// shardtree_support nullifier_map sapling_memo_consistency
// | |
// add_account_birthdays v_transactions_transparent_history
// | |
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
// | |
// wallet_summaries v_transactions_shielding_balance
// |
// v_transactions_note_uniqueness
// received_notes_nullable_nf
// / | \
// shardtree_support nullifier_map sapling_memo_consistency
// / \ |
// add_account_birthdays receiving_key_scopes v_transactions_transparent_history
// | |
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
// | |
// wallet_summaries v_transactions_shielding_balance
// |
// v_transactions_note_uniqueness
vec![
Box::new(initial_setup::Migration {}),
Box::new(utxos_table::Migration {}),
@ -86,5 +87,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
Box::new(v_tx_outputs_use_legacy_false::Migration),
Box::new(v_transactions_shielding_balance::Migration),
Box::new(v_transactions_note_uniqueness::Migration),
Box::new(receiving_key_scopes::Migration {
params: params.clone(),
}),
]
}

View File

@ -0,0 +1,111 @@
//! This migration adds support for the Orchard protocol to `zcash_client_sqlite`
use std::collections::HashSet;
use rusqlite::{self, named_params};
use schemer;
use schemer_rusqlite::RusqliteMigration;
use tracing::debug;
use uuid::Uuid;
use zcash_client_backend::{keys::UnifiedFullViewingKey, scanning::ScanningKey};
use zcash_primitives::{
consensus::{self, sapling_zip212_enforcement, BlockHeight, BranchId},
sapling::note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
transaction::Transaction,
zip32::Scope,
};
use crate::wallet::{
init::{migrations::shardtree_support, WalletMigrationError},
scope_code,
};
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xee89ed2b_c1c2_421e_9e98_c1e3e54a7fc2);
pub(super) struct Migration<P> {
pub(super) params: P,
}
impl<P> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[shardtree_support::MIGRATION_ID].into_iter().collect()
}
fn description(&self) -> &'static str {
"Add support for receiving storage of note commitment tree data using the `shardtree` crate."
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// Add commitment tree sizes to block metadata.
debug!("Adding new columns");
transaction.execute_batch(
&format!(
"ALTER TABLE sapling_received_notes ADD COLUMN recipient_key_scope INTEGER NOT NULL DEFAULT {};",
scope_code(Scope::External)
)
)?;
// For all notes marked as change, we have to determine whether they were actually sent to
// the internal key or the external key for the account, so we trial-decrypt the original
// output with both and pick the scope of whichever worked.
let mut stmt_select_notes = transaction.prepare(
"SELECT id_note, output_index, transactions.raw, transactions.expiry_height, accounts.ufvk
FROM sapling_received_notes
INNER JOIN accounts on accounts.account = sapling_received_notes.account
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
WHERE is_change = 1"
)?;
let mut rows = stmt_select_notes.query([])?;
while let Some(row) = rows.next()? {
let note_id: i64 = row.get(0)?;
let output_index: usize = row.get(1)?;
let tx_data: Vec<u8> = row.get(2)?;
let tx = Transaction::read(&tx_data[..], BranchId::Canopy)
.expect("Transaction must be valid");
let output = tx
.sapling_bundle()
.and_then(|b| b.shielded_outputs().get(output_index))
.unwrap_or_else(|| panic!("A Sapling output must exist at index {}", output_index));
let tx_expiry_height = BlockHeight::from(row.get::<_, u32>(3)?);
let zip212_enforcement = sapling_zip212_enforcement(&self.params, tx_expiry_height);
let ufvk_str: String = row.get(4)?;
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
.expect("Stored UFVKs must be valid");
let dfvk = ufvk
.sapling()
.expect("UFVK must have a Sapling component to have received Sapling notes");
let keys = dfvk.to_sapling_keys();
for (scope, ivk, _) in keys {
let pivk = PreparedIncomingViewingKey::new(&ivk);
if try_sapling_note_decryption(&pivk, output, zip212_enforcement).is_some() {
transaction.execute(
"UPDATE sapling_received_notes SET recipient_key_scope = :scope
WHERE id_note = :note_id",
named_params! {":scope": scope_code(scope), ":note_id": note_id},
)?;
}
}
}
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic?
panic!("Cannot revert this migration.");
}
}