zcash_client_sqlite: Add a table to track transaction status and enrichment requests.

This commit is contained in:
Kris Nuttycombe 2024-07-29 15:07:36 -06:00
parent 301e8b497c
commit 1057ddb516
9 changed files with 257 additions and 8 deletions

View File

@ -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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)
}
// 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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
#[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<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
&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,10 +1463,48 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
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(())
})

View File

@ -1858,6 +1858,8 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
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,15 +1869,19 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
// 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")]
{
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"))]
panic!("Sent a transaction with Orchard Actions without `orchard` enabled?");
@ -1965,6 +1971,18 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
}
}
// 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<Self> {
match code {
0 => Some(TxQueryType::Status),
1 => Some(TxQueryType::Enhancement),
_ => None,
}
}
}
pub(crate) fn queue_tx_retrieval(
conn: &rusqlite::Transaction<'_>,
txids: impl Iterator<Item = TxId>,
query_type: TxQueryType,
dependent_tx_ref: Option<TxRef>,
) -> 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<P: consensus::Parameters>(

View File

@ -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
//

View File

@ -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();

View File

@ -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<P: consensus::Parameters + 'static>(
// 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<P: consensus::Parameters + 'static>(
params: params.clone(),
}),
Box::new(spend_key_available::Migration),
Box::new(tx_retrieval_queue::Migration),
]
}

View File

@ -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<Uuid> {
[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]);
}
}

View File

@ -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<Option<WalletTransparentOutput>, SqliteClientError> = stmt_select_utxo

View File

@ -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.

View File

@ -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)]