From d3837d4dfb3eb71466584705cbbaff704c62aecc Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 1 Aug 2024 16:44:44 -0600 Subject: [PATCH] zcash_client_sqlite: Add queue for transparent spend detection by address/outpoint. --- zcash_client_sqlite/src/lib.rs | 19 ++++- zcash_client_sqlite/src/wallet/db.rs | 17 +++++ zcash_client_sqlite/src/wallet/init.rs | 1 + .../init/migrations/tx_retrieval_queue.rs | 13 +++- zcash_client_sqlite/src/wallet/transparent.rs | 70 +++++++++++++++++-- 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index f7f6ad0262..6dfab3ffe7 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -595,8 +595,10 @@ impl, P: consensus::Parameters> WalletRead for W let iter = std::iter::empty(); #[cfg(feature = "transparent-inputs")] - let iter = iter - .chain(wallet::transparent::transaction_data_requests(self.conn.borrow())?.into_iter()); + let iter = iter.chain( + wallet::transparent::transaction_data_requests(self.conn.borrow(), &self.params)? + .into_iter(), + ); Ok(iter.collect()) } @@ -1423,6 +1425,19 @@ impl WalletWrite for WalletDb // that any transparent inputs belonging to the wallet will be // discovered. tx_has_wallet_outputs = true; + + // When we receive transparent funds (particularly as ephemeral outputs + // in transaction pairs sending to a ZIP 320 address) it becomes + // possible that the spend of these outputs is not then later detected + // if the transaction that spends them is purely transparent. This is + // particularly a problem in wallet recovery. + wallet::transparent::queue_transparent_spend_detection( + wdb.conn.0, + &wdb.params, + address, + tx_ref, + output_index.try_into().unwrap() + )?; } // If a transaction we observe contains spends from our wallet, we will diff --git a/zcash_client_sqlite/src/wallet/db.rs b/zcash_client_sqlite/src/wallet/db.rs index 3c5a34e9d0..41fa04ca2d 100644 --- a/zcash_client_sqlite/src/wallet/db.rs +++ b/zcash_client_sqlite/src/wallet/db.rs @@ -411,6 +411,23 @@ CREATE TABLE tx_retrieval_queue ( FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx) )"#; +/// Stores the set of transaction outputs received by the wallet for which spend information +/// (if any) should be retrieved. +/// +/// This table is populated in the process of wallet recovery when a deshielding transaction +/// with transparent outputs belonging to the wallet (i.e., the deshielding half of a ZIP 320 +/// transaction pair) is discovered. It is expected that such a transparent output will be +/// spent soon after it is received in a purely-transparent transaction, which the wallet +/// currently has no means of detecting otherwise. +pub(super) const TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE: &str = r#" +CREATE TABLE transparent_spend_search_queue ( + address TEXT NOT NULL, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + CONSTRAINT value_received_height UNIQUE (transaction_id, output_index) +)"#; + // // State for shard trees // diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index c63a1591e5..9832a04ef1 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -406,6 +406,7 @@ mod tests { db::TABLE_TRANSACTIONS, db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS, db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS, + db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE, db::TABLE_TX_LOCATOR_MAP, db::TABLE_TX_RETRIEVAL_QUEUE, ]; diff --git a/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs index 06315303c1..8da5d4b9cc 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs @@ -41,7 +41,15 @@ impl RusqliteMigration for Migration { ); ALTER TABLE transactions ADD COLUMN confirmed_unmined INTEGER; - ALTER TABLE transactions ADD COLUMN target_height INTEGER;", + ALTER TABLE transactions ADD COLUMN target_height INTEGER; + + CREATE TABLE transparent_spend_search_queue ( + address TEXT NOT NULL, + transaction_id INTEGER NOT NULL, + output_index INTEGER NOT NULL, + FOREIGN KEY (transaction_id) REFERENCES transactions(id_tx), + CONSTRAINT value_received_height UNIQUE (transaction_id, output_index) + );", )?; transaction.execute( @@ -56,7 +64,8 @@ impl RusqliteMigration for Migration { fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { transaction.execute_batch( - "ALTER TABLE transactions DROP COLUMN target_height; + "DROP TABLE transparent_spend_search_queue; + ALTER TABLE transactions DROP COLUMN target_height; ALTER TABLE transactions DROP COLUMN confirmed_unmined; DROP TABLE tx_retrieval_queue;", )?; diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index 716f3283c6..530122518e 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -443,14 +443,26 @@ pub(crate) fn mark_transparent_utxo_spent( AND txo.output_index = :prevout_idx ON CONFLICT (transparent_received_output_id, transaction_id) DO NOTHING", )?; - - let sql_args = named_params![ + stmt_mark_transparent_utxo_spent.execute(named_params![ ":spent_in_tx": tx_ref.0, - ":prevout_txid": &outpoint.hash().to_vec(), - ":prevout_idx": &outpoint.n(), - ]; + ":prevout_txid": outpoint.hash().as_ref(), + ":prevout_idx": outpoint.n(), + ])?; + + // Since we know that the output is spent, we no longer need to search for + // it to find out if it has been spent. + let mut stmt_remove_spend_detection = conn.prepare_cached( + "DELETE FROM transparent_spend_search_queue + WHERE output_index = :prevout_idx + AND transaction_id IN ( + SELECT id_tx FROM transactions WHERE txid = :prevout_txid + )", + )?; + stmt_remove_spend_detection.execute(named_params![ + ":prevout_txid": outpoint.hash().as_ref(), + ":prevout_idx": outpoint.n(), + ])?; - stmt_mark_transparent_utxo_spent.execute(sql_args)?; Ok(()) } @@ -479,11 +491,19 @@ pub(crate) fn put_received_transparent_utxo( } /// Returns the vector of [`TxId`]s for transactions for which spentness state is indeterminate. -pub(crate) fn transaction_data_requests( +pub(crate) fn transaction_data_requests( conn: &rusqlite::Connection, + params: &P, ) -> Result, SqliteClientError> { let mut tx_retrieval_stmt = conn.prepare_cached("SELECT txid, query_type FROM tx_retrieval_queue")?; + let mut address_request_stmt = conn.prepare_cached( + "SELECT ssq.address, t.target_height + FROM transparent_spend_search_queue ssq + JOIN transactions t ON t.id_tx = ssq.transaction_id + WHERE t.mined_height IS NULL + AND t.target_height IS NOT NULL", + )?; let result = tx_retrieval_stmt .query_and_then([], |row| { @@ -499,6 +519,15 @@ pub(crate) fn transaction_data_requests( TxQueryType::Status => TransactionDataRequest::GetStatus(txid), }) })? + .chain(address_request_stmt.query_and_then([], |row| { + let address = TransparentAddress::decode(params, &row.get::<_, String>(0)?)?; + let block_range_start = BlockHeight::from(row.get::<_, u32>(1)?); + Ok(TransactionDataRequest::SpendsFromAddress { + address, + block_range_start, + block_range_end: Some(block_range_start + DEFAULT_TX_EXPIRY_DELTA), + }) + })?) .collect::, _>>()?; Ok(result) @@ -740,6 +769,33 @@ pub(crate) fn queue_transparent_spend_detection( Ok(()) } +pub(crate) fn queue_transparent_spend_detection( + conn: &rusqlite::Transaction<'_>, + params: &P, + receiving_address: TransparentAddress, + tx_ref: TxRef, + output_index: u32, +) -> Result<(), SqliteClientError> { + // Add an entry to the transaction retrieval queue if we don't already have raw transaction + // data. + let mut stmt = conn.prepare_cached( + "INSERT INTO transparent_spend_search_queue + (address, transaction_id, output_index) + VALUES + (:address, :transaction_id, :output_index) + ON CONFLICT (transaction_id, output_index) DO NOTHING", + )?; + + let addr_str = receiving_address.encode(params); + stmt.execute(named_params! { + ":address": addr_str, + ":transaction_id": tx_ref.0, + ":output_index": output_index + })?; + + Ok(()) +} + #[cfg(test)] mod tests { use crate::testing::{AddressType, TestBuilder, TestState};