zcash_client_sqlite: Add a table to track transaction status and enrichment requests.
This commit is contained in:
parent
301e8b497c
commit
1057ddb516
|
@ -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,11 +1463,49 @@ 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(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,14 +1869,18 @@ 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")]
|
||||
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<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>(
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue