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 5, 2024
1 parent f70d90d commit 4741662
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 11 deletions.
14 changes: 14 additions & 0 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,20 @@ pub enum TransactionDataRequest {
/// The caller should respond to this request by calling [`WalletWrite::set_transaction_status`]
/// to provide transaction status information to the wallet backend.
GetStatus(TxId),
/// Information about transactions that receive or spend funds belonging to the specified
/// transparent address is requested.
///
/// Fully transparent transactions, and transactions that do not contain either shielded inputs
/// or shielded outputs belonging to the wallet, may not be discovered by the process of chain
/// scanning; as a consequence, the wallet must actively query to find transactions that spend
/// such funds. Ideally we'd be able to query by [`OutPoint`] but this is not currently
/// functionality that is supported by the light wallet server.
#[cfg(feature = "transparent-inputs")]
SpendsFromAddress {
address: TransparentAddress,
block_range_start: BlockHeight,
block_range_end: Option<BlockHeight>,
},
}

/// Metadata about the status of a transaction obtained by inspecting the chain state.
Expand Down
19 changes: 17 additions & 2 deletions zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,10 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
let iter = std::iter::empty();

Check warning on line 596 in zcash_client_sqlite/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/lib.rs#L595-L596

Added lines #L595 - L596 were not covered by tests

#[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(),

Check warning on line 601 in zcash_client_sqlite/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/lib.rs#L599-L601

Added lines #L599 - L601 were not covered by tests
);

Ok(iter.collect())

Check warning on line 604 in zcash_client_sqlite/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/lib.rs#L604

Added line #L604 was not covered by tests
}
Expand Down Expand Up @@ -1436,6 +1438,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 @@ -410,6 +410,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 @@ -40,7 +40,15 @@ impl RusqliteMigration for Migration {
FOREIGN KEY (dependent_transaction_id) REFERENCES transactions(id_tx)
);
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 @@ -55,7 +63,8 @@ impl RusqliteMigration for Migration {

fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(

Check warning on line 65 in zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/init/migrations/tx_retrieval_queue.rs#L65

Added line #L65 was not covered by tests
"ALTER TABLE transactions DROP COLUMN target_height;
"DROP TABLE transparent_spend_search_queue;
ALTER TABLE transactions DROP COLUMN target_height;
DROP TABLE tx_retrieval_queue;",
)?;

Expand Down
71 changes: 64 additions & 7 deletions zcash_client_sqlite/src/wallet/transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -479,11 +492,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>(

Check warning on line 495 in zcash_client_sqlite/src/wallet/transparent.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/transparent.rs#L495

Added line #L495 was not covered by tests
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(

Check warning on line 501 in zcash_client_sqlite/src/wallet/transparent.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/transparent.rs#L499-L501

Added lines #L499 - L501 were not covered by tests
"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 +520,15 @@ pub(crate) fn transaction_data_requests(
TxQueryType::Status => TransactionDataRequest::GetStatus(txid),

Check warning on line 520 in zcash_client_sqlite/src/wallet/transparent.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/transparent.rs#L518-L520

Added lines #L518 - L520 were not covered by tests
})
})?
.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),

Check warning on line 529 in zcash_client_sqlite/src/wallet/transparent.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/transparent.rs#L523-L529

Added lines #L523 - L529 were not covered by tests
})
})?)
.collect::<Result<Vec<_>, _>>()?;

Ok(result)

Check warning on line 534 in zcash_client_sqlite/src/wallet/transparent.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet/transparent.rs#L534

Added line #L534 was not covered by tests
Expand Down Expand Up @@ -713,6 +743,33 @@ pub(crate) fn put_transparent_output<P: consensus::Parameters>(
Ok(utxo_id)
}

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 4741662

Please sign in to comment.