Skip to content

Commit

Permalink
zcash_client_sqlite: Add queue for transparent spend detection by add…
Browse files Browse the repository at this point in the history
…ress/outpoint.
  • Loading branch information
nuttycom committed Aug 2, 2024
1 parent 8989446 commit d3837d4
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 11 deletions.
19 changes: 17 additions & 2 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,10 @@ impl<C: Borrow<rusqlite::Connection>, 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())
}
Expand Down Expand Up @@ -1423,6 +1425,19 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// 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
Expand Down
17 changes: 17 additions & 0 deletions zcash_client_sqlite/src/wallet/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down
1 change: 1 addition & 0 deletions zcash_client_sqlite/src/wallet/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;",
)?;
Expand Down
70 changes: 63 additions & 7 deletions zcash_client_sqlite/src/wallet/transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -479,11 +491,19 @@ pub(crate) fn put_received_transparent_utxo<P: consensus::Parameters>(
}

/// 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<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
) -> Result<Vec<TransactionDataRequest>, 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| {
Expand All @@ -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::<Result<Vec<_>, _>>()?;

Ok(result)
Expand Down Expand Up @@ -740,6 +769,33 @@ pub(crate) fn queue_transparent_spend_detection<P: consensus::Parameters>(
Ok(())
}

pub(crate) fn queue_transparent_spend_detection<P: consensus::Parameters>(
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};
Expand Down

0 comments on commit d3837d4

Please sign in to comment.