From 1057ddb516cb86c491dfe03756c3c5c7c069ab6a Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 29 Jul 2024 15:07:36 -0600 Subject: [PATCH] zcash_client_sqlite: Add a table to track transaction status and enrichment requests. --- zcash_client_sqlite/src/lib.rs | 75 ++++++++++++++- zcash_client_sqlite/src/wallet.rs | 92 ++++++++++++++++++- zcash_client_sqlite/src/wallet/db.rs | 19 ++++ zcash_client_sqlite/src/wallet/init.rs | 1 + .../src/wallet/init/migrations.rs | 6 +- .../init/migrations/tx_retrieval_queue.rs | 61 ++++++++++++ zcash_client_sqlite/src/wallet/transparent.rs | 3 +- zcash_primitives/CHANGELOG.md | 1 + .../src/transaction/components/transparent.rs | 7 +- 9 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 1655d1be3..cef2e8a9f 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -118,7 +118,7 @@ pub mod error; pub mod wallet; use wallet::{ commitment_tree::{self, put_shard_roots}, - SubtreeScanProgress, + notify_tx_retrieved, SubtreeScanProgress, TxQueryType, }; #[cfg(feature = "transparent-inputs")] @@ -787,6 +787,12 @@ impl WalletWrite for WalletDb for tx in block.transactions() { let tx_row = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?; + wallet::queue_tx_retrieval( + wdb.conn.0, + std::iter::once(tx.txid()), + TxQueryType::Enhancement, + None, + )?; // Mark notes as spent and remove them from the scanning cache for spend in tx.sapling_spends() { @@ -1183,7 +1189,27 @@ impl WalletWrite for WalletDb ) } + // A flag used to determine whether it is necessary to query for transactions that + // provided transparent inputs to this transaction, in order to be able to correctly + // recover transparent transaction history. + #[cfg(feature = "transparent-inputs")] + let mut tx_has_wallet_outputs = false; + + #[cfg(feature = "transparent-inputs")] + let detectable_via_scanning = { + let mut detectable_via_scanning = d_tx.tx().sapling_bundle().is_some(); + #[cfg(feature = "orchard")] { + detectable_via_scanning |= d_tx.tx().orchard_bundle().is_some(); + } + + detectable_via_scanning + }; + for output in d_tx.sapling_outputs() { + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } match output.transfer_type() { TransferType::Outgoing => { let recipient = { @@ -1268,6 +1294,10 @@ impl WalletWrite for WalletDb #[cfg(feature = "orchard")] for output in d_tx.orchard_outputs() { + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } match output.transfer_type() { TransferType::Outgoing => { let recipient = { @@ -1395,6 +1425,11 @@ impl WalletWrite for WalletDb &address, account_id )?; + + // Since the wallet created the transparent output, we need to ensure + // that any transparent inputs belonging to the wallet will be + // discovered. + tx_has_wallet_outputs = true; } // If a transaction we observe contains spends from our wallet, we will @@ -1428,11 +1463,49 @@ impl WalletWrite for WalletDb txout.value, None, )?; + + // Even though we know the funding account, we don't know that we have + // information for all of the transparent inputs to the transaction. + #[cfg(feature = "transparent-inputs")] + { + tx_has_wallet_outputs = true; + } + + // If the decrypted transaction is unmined and has no shielded + // components, add it to the queue for status retrieval. + #[cfg(feature = "transparent-inputs")] + if d_tx.mined_height().is_none() && !detectable_via_scanning { + wallet::queue_tx_retrieval( + wdb.conn.0, + std::iter::once(d_tx.tx().txid()), + TxQueryType::Status, + None + )?; + } } } } } + // If the transaction has outputs that belong to the wallet as well as transparent + // inputs, we may need to download the transactions corresponding to the transparent + // prevout references to determine whether the transaction was created (at least in + // part) by this wallet. + #[cfg(feature = "transparent-inputs")] + if tx_has_wallet_outputs { + if let Some(b) = d_tx.tx().transparent_bundle() { + // queue the transparent inputs for enhancement + wallet::queue_tx_retrieval( + wdb.conn.0, + b.vin.iter().map(|txin| *txin.prevout.txid()), + TxQueryType::Enhancement, + Some(tx_ref) + )?; + } + } + + notify_tx_retrieved(wdb.conn.0, d_tx.tx().txid())?; + Ok(()) }) } diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index f41ec4779..b963d6631 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -1858,6 +1858,8 @@ pub(crate) fn store_transaction_to_be_sent( Some(sent_tx.created()), )?; + let mut detectable_via_scanning = false; + // Mark notes as spent. // // This locks the notes so they aren't selected again by a subsequent call to @@ -1867,14 +1869,18 @@ pub(crate) fn store_transaction_to_be_sent( // Assumes that create_spend_to_address() will never be called in parallel, which is a // reasonable assumption for a light client such as a mobile phone. if let Some(bundle) = sent_tx.tx().sapling_bundle() { + detectable_via_scanning = true; for spend in bundle.shielded_spends() { sapling::mark_sapling_note_spent(wdb.conn.0, tx_ref, spend.nullifier())?; } } if let Some(_bundle) = sent_tx.tx().orchard_bundle() { #[cfg(feature = "orchard")] - for action in _bundle.actions() { - orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, action.nullifier())?; + { + detectable_via_scanning = true; + for action in _bundle.actions() { + orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, action.nullifier())?; + } } #[cfg(not(feature = "orchard"))] @@ -1965,6 +1971,18 @@ pub(crate) fn store_transaction_to_be_sent( } } + // Add the transaction to the set to be queried for transaction status. This is only necessary + // at present for fully-transparent transactions, because any transaction with a shielded + // component will be detected via ordinary chain scanning and/or nullifier checking. + if !detectable_via_scanning { + queue_tx_retrieval( + wdb.conn.0, + std::iter::once(sent_tx.tx().txid()), + TxQueryType::Status, + None, + )?; + } + Ok(()) } @@ -2300,6 +2318,76 @@ pub(crate) fn put_tx_data( .map_err(SqliteClientError::from) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TxQueryType { + Status, + Enhancement, +} + +impl TxQueryType { + pub(crate) fn code(&self) -> i64 { + match self { + TxQueryType::Status => 0, + TxQueryType::Enhancement => 1, + } + } + + pub(crate) fn from_code(code: i64) -> Option { + match code { + 0 => Some(TxQueryType::Status), + 1 => Some(TxQueryType::Enhancement), + _ => None, + } + } +} + +pub(crate) fn queue_tx_retrieval( + conn: &rusqlite::Transaction<'_>, + txids: impl Iterator, + query_type: TxQueryType, + dependent_tx_ref: Option, +) -> Result<(), SqliteClientError> { + // Add an entry to the transaction retrieval queue if we don't already have raw transaction data. + let mut stmt_insert_tx = conn.prepare_cached( + "INSERT INTO tx_retrieval_queue (txid, query_type, dependent_transaction_id) + SELECT :txid, :query_type, :dependent_transaction_id + -- do not queue enhancement requests if we already have the raw transaction + WHERE NOT EXISTS ( + SELECT 1 FROM transactions + WHERE txid = :txid + AND :query_type = :enhancement_type + AND raw IS NOT NULL + ) + -- if there is already a status request, we can upgrade it to an enhancement request + ON CONFLICT (txid) DO UPDATE + SET query_type = MAX(:query_type, query_type), + dependent_transaction_id = IFNULL(:dependent_transaction_id, dependent_transaction_id)", + )?; + + for txid in txids { + stmt_insert_tx.execute(named_params! { + ":txid": txid.as_ref(), + ":query_type": query_type.code(), + ":dependent_transaction_id": dependent_tx_ref.map(|r| r.0), + ":enhancement_type": TxQueryType::Enhancement.code(), + })?; + } + + Ok(()) +} + +pub(crate) fn notify_tx_retrieved( + conn: &rusqlite::Connection, + txid: TxId, +) -> Result<(), SqliteClientError> { + conn.execute( + "DELETE FROM tx_retrieval_queue WHERE txid = :txid", + named_params![":txid": &txid.as_ref()[..]], + )?; + + Ok(()) +} + // A utility function for creation of parameters for use in `insert_sent_output` // and `put_sent_output` fn recipient_params( diff --git a/zcash_client_sqlite/src/wallet/db.rs b/zcash_client_sqlite/src/wallet/db.rs index e713fb965..0b0f4613c 100644 --- a/zcash_client_sqlite/src/wallet/db.rs +++ b/zcash_client_sqlite/src/wallet/db.rs @@ -390,6 +390,25 @@ pub(super) const INDEX_SENT_NOTES_TO_ACCOUNT: &str = r#"CREATE INDEX sent_notes_to_account ON "sent_notes" (to_account_id)"#; pub(super) const INDEX_SENT_NOTES_TX: &str = r#"CREATE INDEX sent_notes_tx ON "sent_notes" (tx)"#; +/// Stores the set of transaction ids for which the backend required additional data. +/// +/// ### Columns: +/// - `txid`: The transaction identifier for the transaction to retrieve state information for. +/// - `query_type`: +/// - `0` for raw transaction (enhancement) data, +/// - `1` for transaction mined-ness information. +/// - `dependent_transaction_id`: If the transaction data request is searching for information +/// about transparent inputs to a transaction, this is a reference to that transaction record. +/// NULL for transactions where the request for enhancement data is based on discovery due +/// to blockchain scanning. +pub(super) const TABLE_TX_RETRIEVAL_QUEUE: &str = r#" +CREATE TABLE tx_retrieval_queue ( + txid BLOB NOT NULL UNIQUE, + query_type INTEGER NOT NULL, + dependent_transaction_id INTEGER, + FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx) +)"#; + // // State for shard trees // diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index ea6c935c2..c63a1591e 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -407,6 +407,7 @@ mod tests { db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS, db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS, db::TABLE_TX_LOCATOR_MAP, + db::TABLE_TX_RETRIEVAL_QUEUE, ]; let rows = describe_tables(&st.wallet().conn).unwrap(); diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index ad533c025..43bd77d87 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -15,6 +15,7 @@ mod sapling_memo_consistency; mod sent_notes_to_internal; mod shardtree_support; mod spend_key_available; +mod tx_retrieval_queue; mod ufvk_support; mod utxos_table; mod utxos_to_txos; @@ -68,8 +69,8 @@ pub(super) fn all_migrations( // orchard_received_notes spend_key_available // / \ // ensure_orchard_ua_receiver utxos_to_txos - // | - // ephemeral_addresses + // / \ + // ephemeral_addresses tx_retrieval_queue vec![ Box::new(initial_setup::Migration {}), Box::new(utxos_table::Migration {}), @@ -124,6 +125,7 @@ pub(super) fn all_migrations( params: params.clone(), }), Box::new(spend_key_available::Migration), + Box::new(tx_retrieval_queue::Migration), ] } 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 new file mode 100644 index 000000000..1ce330bab --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs @@ -0,0 +1,61 @@ +//! A migration to add the `tx_retrieval_queue` table to the database. + +use rusqlite::Transaction; +use schemer_rusqlite::RusqliteMigration; +use std::collections::HashSet; +use uuid::Uuid; + +use crate::wallet::init::WalletMigrationError; + +use super::utxos_to_txos; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xfec02b61_3988_4b4f_9699_98977fac9e7f); + +pub(crate) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [utxos_to_txos::MIGRATION_ID].into_iter().collect() + } + + fn description(&self) -> &'static str { + "Adds a table for tracking transactions to be downloaded for transparent output and/or memo retrieval." + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch( + "CREATE TABLE tx_retrieval_queue ( + txid BLOB NOT NULL UNIQUE, + query_type INTEGER NOT NULL, + dependent_transaction_id INTEGER, + FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx) + );", + )?; + + Ok(()) + } + + fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { + transaction.execute_batch("DROP TABLE tx_retrieval_queue;")?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::wallet::init::migrations::tests::test_migrate; + + #[test] + fn migrate() { + test_migrate(&[super::MIGRATION_ID]); + } +} diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index 4f0d95b9b..a83967a25 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -228,8 +228,7 @@ pub(crate) fn get_wallet_transparent_output( OR tx.expiry_height >= :mempool_height -- the spending tx has not yet expired ) ) - ) - ", + )", )?; let result: Result, SqliteClientError> = stmt_select_utxo diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index ddd5850c7..9d8db7bae 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -20,6 +20,7 @@ and this library adheres to Rust's notion of - `InputSize` - `InputView::serialized_size` - `OutputView::serialized_size` +- `zcash_primitives::transaction::component::transparent::OutPoint::txid` ### Changed - MSRV is now 1.70.0. diff --git a/zcash_primitives/src/transaction/components/transparent.rs b/zcash_primitives/src/transaction/components/transparent.rs index 1c140f4f1..e2c82fd84 100644 --- a/zcash_primitives/src/transaction/components/transparent.rs +++ b/zcash_primitives/src/transaction/components/transparent.rs @@ -142,10 +142,15 @@ impl OutPoint { self.n } - /// Returns the txid of the transaction containing this `OutPoint`. + /// Returns the byte representation of the txid of the transaction containing this `OutPoint`. pub fn hash(&self) -> &[u8; 32] { self.hash.as_ref() } + + /// Returns the txid of the transaction containing this `OutPoint`. + pub fn txid(&self) -> &TxId { + &self.hash + } } #[derive(Debug, Clone, PartialEq)]