diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 159cbfa3c..44e5226f4 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -596,8 +596,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()) } @@ -1429,6 +1431,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 3c5a34e9d..41fa04ca2 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 c63a1591e..9832a04ef 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 06315303c..8da5d4b9c 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 716f3283c..0e6e4f456 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet}; use rusqlite::OptionalExtension; use rusqlite::{named_params, Connection, Row}; use zcash_client_backend::data_api::TransactionDataRequest; +use zcash_primitives::transaction::builder::DEFAULT_TX_EXPIRY_DELTA; use zcash_primitives::transaction::TxId; use zip32::{DiversifierIndex, Scope}; @@ -443,14 +444,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 +492,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 +520,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)