Merge pull request #619 from zcash/sqlite-prepared-statement-type-safety

`zcash_client_sqlite`: Improve type safety for prepared statements
This commit is contained in:
str4d 2022-08-29 20:57:19 +01:00 committed by GitHub
commit b4fc235a2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 645 additions and 296 deletions

View File

@ -36,7 +36,7 @@ use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use rusqlite::{Connection, Statement, NO_PARAMS};
use rusqlite::{Connection, NO_PARAMS};
use zcash_primitives::{
block::BlockHash,
@ -69,6 +69,9 @@ use {
zcash_primitives::legacy::TransparentAddress,
};
mod prepared;
pub use prepared::DataConnStmtCache;
pub mod chain;
pub mod error;
pub mod wallet;
@ -116,92 +119,7 @@ impl<P: consensus::Parameters> WalletDb<P> {
/// for that database. This operation may eagerly initialize and cache sqlite
/// prepared statements that are used in write operations.
pub fn get_update_ops(&self) -> Result<DataConnStmtCache<'_, P>, SqliteClientError> {
Ok(
DataConnStmtCache {
wallet_db: self,
stmt_insert_block: self.conn.prepare(
"INSERT INTO blocks (height, hash, time, sapling_tree)
VALUES (?, ?, ?, ?)",
)?,
stmt_insert_tx_meta: self.conn.prepare(
"INSERT INTO transactions (txid, block, tx_index)
VALUES (?, ?, ?)",
)?,
stmt_update_tx_meta: self.conn.prepare(
"UPDATE transactions
SET block = ?, tx_index = ? WHERE txid = ?",
)?,
stmt_insert_tx_data: self.conn.prepare(
"INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES (?, ?, ?, ?)",
)?,
stmt_update_tx_data: self.conn.prepare(
"UPDATE transactions
SET expiry_height = ?, raw = ? WHERE txid = ?",
)?,
stmt_select_tx_ref: self.conn.prepare(
"SELECT id_tx FROM transactions WHERE txid = ?",
)?,
stmt_mark_sapling_note_spent: self.conn.prepare(
"UPDATE received_notes SET spent = ? WHERE nf = ?"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_mark_transparent_utxo_spent: self.conn.prepare(
"UPDATE utxos SET spent_in_tx = :spent_in_tx
WHERE prevout_txid = :prevout_txid
AND prevout_idx = :prevout_idx"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: self.conn.prepare(
"INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height)
VALUES (:address, :prevout_txid, :prevout_idx, :script, :value_zat, :height)"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: self.conn.prepare(
"DELETE FROM utxos WHERE address = :address AND height > :above_height"
)?,
stmt_insert_received_note: self.conn.prepare(
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
)?,
stmt_update_received_note: self.conn.prepare(
"UPDATE received_notes
SET account = :account,
diversifier = :diversifier,
value = :value,
rcm = :rcm,
nf = IFNULL(:nf, nf),
memo = IFNULL(:memo, memo),
is_change = IFNULL(:is_change, is_change)
WHERE tx = :tx AND output_index = :output_index",
)?,
stmt_select_received_note: self.conn.prepare(
"SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?"
)?,
stmt_update_sent_note: self.conn.prepare(
"UPDATE sent_notes
SET from_account = ?, address = ?, value = ?, memo = ?
WHERE tx = ? AND output_pool = ? AND output_index = ?",
)?,
stmt_insert_sent_note: self.conn.prepare(
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo)
VALUES (?, ?, ?, ?, ?, ?, ?)",
)?,
stmt_insert_witness: self.conn.prepare(
"INSERT INTO sapling_witnesses (note, block, witness)
VALUES (?, ?, ?)",
)?,
stmt_prune_witnesses: self.conn.prepare(
"DELETE FROM sapling_witnesses WHERE block < ?"
)?,
stmt_update_expired: self.conn.prepare(
"UPDATE received_notes SET spent = NULL WHERE EXISTS (
SELECT id_tx FROM transactions
WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ?
)",
)?,
}
)
DataConnStmtCache::new(self)
}
}
@ -326,44 +244,6 @@ impl<P: consensus::Parameters> WalletReadTransparent for WalletDb<P> {
}
}
/// The primary type used to implement [`WalletWrite`] for the SQLite database.
///
/// A data structure that stores the SQLite prepared statements that are
/// required for the implementation of [`WalletWrite`] against the backing
/// store.
///
/// [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
pub struct DataConnStmtCache<'a, P> {
wallet_db: &'a WalletDb<P>,
stmt_insert_block: Statement<'a>,
stmt_insert_tx_meta: Statement<'a>,
stmt_update_tx_meta: Statement<'a>,
stmt_insert_tx_data: Statement<'a>,
stmt_update_tx_data: Statement<'a>,
stmt_select_tx_ref: Statement<'a>,
stmt_mark_sapling_note_spent: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_mark_transparent_utxo_spent: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: Statement<'a>,
stmt_insert_received_note: Statement<'a>,
stmt_update_received_note: Statement<'a>,
stmt_select_received_note: Statement<'a>,
stmt_insert_sent_note: Statement<'a>,
stmt_update_sent_note: Statement<'a>,
stmt_insert_witness: Statement<'a>,
stmt_prune_witnesses: Statement<'a>,
stmt_update_expired: Statement<'a>,
}
impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
type Error = SqliteClientError;
type NoteRef = NoteId;

View File

@ -0,0 +1,557 @@
//! Prepared SQL statements used by the wallet.
//!
//! Some `rusqlite` crate APIs are only available on prepared statements; these are stored
//! inside the [`DataConnStmtCache`]. When adding a new prepared statement:
//!
//! - Add it as a private field of `DataConnStmtCache`.
//! - Build the statement in [`DataConnStmtCache::new`].
//! - Add a crate-private helper method to `DataConnStmtCache` for running the statement.
use rusqlite::{params, Statement, ToSql};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
memo::MemoBytes,
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Diversifier, Node, Nullifier},
transaction::{components::Amount, TxId},
zip32::AccountId,
};
use crate::{error::SqliteClientError, wallet::PoolType, NoteId, WalletDb};
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::{
legacy::TransparentAddress, transaction::components::transparent::OutPoint,
},
};
/// The primary type used to implement [`WalletWrite`] for the SQLite database.
///
/// A data structure that stores the SQLite prepared statements that are
/// required for the implementation of [`WalletWrite`] against the backing
/// store.
///
/// [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
pub struct DataConnStmtCache<'a, P> {
pub(crate) wallet_db: &'a WalletDb<P>,
stmt_insert_block: Statement<'a>,
stmt_insert_tx_meta: Statement<'a>,
stmt_update_tx_meta: Statement<'a>,
stmt_insert_tx_data: Statement<'a>,
stmt_update_tx_data: Statement<'a>,
stmt_select_tx_ref: Statement<'a>,
stmt_mark_sapling_note_spent: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_mark_transparent_utxo_spent: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: Statement<'a>,
stmt_insert_received_note: Statement<'a>,
stmt_update_received_note: Statement<'a>,
stmt_select_received_note: Statement<'a>,
stmt_insert_sent_note: Statement<'a>,
stmt_update_sent_note: Statement<'a>,
stmt_insert_witness: Statement<'a>,
stmt_prune_witnesses: Statement<'a>,
stmt_update_expired: Statement<'a>,
}
impl<'a, P> DataConnStmtCache<'a, P> {
pub(crate) fn new(wallet_db: &'a WalletDb<P>) -> Result<Self, SqliteClientError> {
Ok(
DataConnStmtCache {
wallet_db,
stmt_insert_block: wallet_db.conn.prepare(
"INSERT INTO blocks (height, hash, time, sapling_tree)
VALUES (?, ?, ?, ?)",
)?,
stmt_insert_tx_meta: wallet_db.conn.prepare(
"INSERT INTO transactions (txid, block, tx_index)
VALUES (?, ?, ?)",
)?,
stmt_update_tx_meta: wallet_db.conn.prepare(
"UPDATE transactions
SET block = ?, tx_index = ? WHERE txid = ?",
)?,
stmt_insert_tx_data: wallet_db.conn.prepare(
"INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES (?, ?, ?, ?)",
)?,
stmt_update_tx_data: wallet_db.conn.prepare(
"UPDATE transactions
SET expiry_height = ?, raw = ? WHERE txid = ?",
)?,
stmt_select_tx_ref: wallet_db.conn.prepare(
"SELECT id_tx FROM transactions WHERE txid = ?",
)?,
stmt_mark_sapling_note_spent: wallet_db.conn.prepare(
"UPDATE received_notes SET spent = ? WHERE nf = ?"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_mark_transparent_utxo_spent: wallet_db.conn.prepare(
"UPDATE utxos SET spent_in_tx = :spent_in_tx
WHERE prevout_txid = :prevout_txid
AND prevout_idx = :prevout_idx"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: wallet_db.conn.prepare(
"INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height)
VALUES (:address, :prevout_txid, :prevout_idx, :script, :value_zat, :height)"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: wallet_db.conn.prepare(
"DELETE FROM utxos WHERE address = :address AND height > :above_height"
)?,
stmt_insert_received_note: wallet_db.conn.prepare(
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
)?,
stmt_update_received_note: wallet_db.conn.prepare(
"UPDATE received_notes
SET account = :account,
diversifier = :diversifier,
value = :value,
rcm = :rcm,
nf = IFNULL(:nf, nf),
memo = IFNULL(:memo, memo),
is_change = IFNULL(:is_change, is_change)
WHERE tx = :tx AND output_index = :output_index",
)?,
stmt_select_received_note: wallet_db.conn.prepare(
"SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?"
)?,
stmt_update_sent_note: wallet_db.conn.prepare(
"UPDATE sent_notes
SET from_account = ?, address = ?, value = ?, memo = ?
WHERE tx = ? AND output_pool = ? AND output_index = ?",
)?,
stmt_insert_sent_note: wallet_db.conn.prepare(
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo)
VALUES (?, ?, ?, ?, ?, ?, ?)",
)?,
stmt_insert_witness: wallet_db.conn.prepare(
"INSERT INTO sapling_witnesses (note, block, witness)
VALUES (?, ?, ?)",
)?,
stmt_prune_witnesses: wallet_db.conn.prepare(
"DELETE FROM sapling_witnesses WHERE block < ?"
)?,
stmt_update_expired: wallet_db.conn.prepare(
"UPDATE received_notes SET spent = NULL WHERE EXISTS (
SELECT id_tx FROM transactions
WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ?
)",
)?,
}
)
}
/// Inserts information about a scanned block into the database.
pub fn stmt_insert_block(
&mut self,
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
commitment_tree: &CommitmentTree<Node>,
) -> Result<(), SqliteClientError> {
let mut encoded_tree = Vec::new();
commitment_tree.write(&mut encoded_tree).unwrap();
self.stmt_insert_block.execute(params![
u32::from(block_height),
&block_hash.0[..],
block_time,
encoded_tree
])?;
Ok(())
}
/// Inserts the given transaction and its block metadata into the wallet.
///
/// Returns the database row for the newly-inserted transaction, or an error if the
/// transaction exists.
pub(crate) fn stmt_insert_tx_meta(
&mut self,
txid: &TxId,
height: BlockHeight,
tx_index: usize,
) -> Result<i64, SqliteClientError> {
self.stmt_insert_tx_meta.execute(params![
&txid.as_ref()[..],
u32::from(height),
(tx_index as i64),
])?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
/// Updates the block metadata for the given transaction.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
pub(crate) fn stmt_update_tx_meta(
&mut self,
height: BlockHeight,
tx_index: usize,
txid: &TxId,
) -> Result<bool, SqliteClientError> {
match self.stmt_update_tx_meta.execute(params![
u32::from(height),
(tx_index as i64),
&txid.as_ref()[..],
])? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("txid column is marked as UNIQUE"),
}
}
/// Inserts the given transaction and its data into the wallet.
///
/// Returns the database row for the newly-inserted transaction, or an error if the
/// transaction exists.
pub(crate) fn stmt_insert_tx_data(
&mut self,
txid: &TxId,
created_at: Option<time::OffsetDateTime>,
expiry_height: BlockHeight,
raw_tx: &[u8],
) -> Result<i64, SqliteClientError> {
self.stmt_insert_tx_data.execute(params![
&txid.as_ref()[..],
created_at,
u32::from(expiry_height),
raw_tx
])?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
/// Updates the data for the given transaction.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
pub(crate) fn stmt_update_tx_data(
&mut self,
expiry_height: BlockHeight,
raw_tx: &[u8],
txid: &TxId,
) -> Result<bool, SqliteClientError> {
match self.stmt_update_tx_data.execute(params![
u32::from(expiry_height),
raw_tx,
&txid.as_ref()[..],
])? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("txid column is marked as UNIQUE"),
}
}
/// Finds the database row for the given `txid`, if the transaction is in the wallet.
pub(crate) fn stmt_select_tx_ref(&mut self, txid: &TxId) -> Result<i64, SqliteClientError> {
self.stmt_select_tx_ref
.query_row(&[&txid.as_ref()[..]], |row| row.get(0))
.map_err(SqliteClientError::from)
}
/// Marks a given nullifier as having been revealed in the construction of the
/// specified transaction.
///
/// Marking a note spent in this fashion does NOT imply that the spending transaction
/// has been mined.
///
/// Returns `false` if the nullifier does not correspond to any received note.
pub(crate) fn stmt_mark_sapling_note_spent(
&mut self,
tx_ref: i64,
nf: &Nullifier,
) -> Result<bool, SqliteClientError> {
match self
.stmt_mark_sapling_note_spent
.execute(params![tx_ref, &nf.0[..]])?
{
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("nf column is marked as UNIQUE"),
}
}
/// Marks the given UTXO as having been spent.
///
/// Returns `false` if `outpoint` does not correspond to any tracked UTXO.
#[cfg(feature = "transparent-inputs")]
pub(crate) fn stmt_mark_transparent_utxo_spent(
&mut self,
tx_ref: i64,
outpoint: &OutPoint,
) -> Result<bool, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":spent_in_tx", &tx_ref),
(":prevout_txid", &outpoint.hash().to_vec()),
(":prevout_idx", &outpoint.n()),
];
match self
.stmt_mark_transparent_utxo_spent
.execute_named(sql_args)?
{
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("tx_outpoint constraint is marked as UNIQUE"),
}
}
}
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
/// Adds the given received UTXO to the datastore.
///
/// Returns the database row for the newly-inserted UTXO, or an error if the UTXO
/// exists.
#[cfg(feature = "transparent-inputs")]
pub(crate) fn stmt_insert_received_transparent_utxo(
&mut self,
output: &WalletTransparentOutput,
) -> Result<i64, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":address", &output.address().encode(&self.wallet_db.params)),
(":prevout_txid", &output.outpoint.hash().to_vec()),
(":prevout_idx", &output.outpoint.n()),
(":script", &output.txout.script_pubkey.0),
(":value_zat", &i64::from(output.txout.value)),
(":height", &u32::from(output.height)),
];
self.stmt_insert_received_transparent_utxo
.execute_named(sql_args)?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
/// Removes all records of UTXOs that were recorded as having been received at block
/// heights greater than the given height.
///
/// Returns the number of UTXOs that were removed.
#[cfg(feature = "transparent-inputs")]
pub(crate) fn stmt_delete_utxos(
&mut self,
taddr: &TransparentAddress,
height: BlockHeight,
) -> Result<usize, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":address", &taddr.encode(&self.wallet_db.params)),
(":above_height", &u32::from(height)),
];
let rows = self.stmt_delete_utxos.execute_named(sql_args)?;
Ok(rows)
}
}
impl<'a, P> DataConnStmtCache<'a, P> {
/// Inserts the given received note into the wallet.
///
/// This implementation relies on the facts that:
/// - A transaction will not contain more than 2^63 shielded outputs.
/// - A note value will never exceed 2^63 zatoshis.
///
/// Returns the database row for the newly-inserted note, or an error if the note
/// exists.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_insert_received_note(
&mut self,
tx_ref: i64,
output_index: usize,
account: AccountId,
diversifier: &Diversifier,
value: u64,
rcm: [u8; 32],
nf: &Option<Nullifier>,
memo: Option<&MemoBytes>,
is_change: Option<bool>,
) -> Result<NoteId, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":tx", &tx_ref),
(":output_index", &(output_index as i64)),
(":account", &u32::from(account)),
(":diversifier", &diversifier.0.as_ref()),
(":value", &(value as i64)),
(":rcm", &rcm.as_ref()),
(":nf", &nf.as_ref().map(|nf| nf.0.as_ref())),
(":memo", &memo.map(|m| m.as_slice())),
(":is_change", &is_change),
];
self.stmt_insert_received_note.execute_named(sql_args)?;
Ok(NoteId::ReceivedNoteId(
self.wallet_db.conn.last_insert_rowid(),
))
}
/// Updates the data for the given transaction.
///
/// This implementation relies on the facts that:
/// - A transaction will not contain more than 2^63 shielded outputs.
/// - A note value will never exceed 2^63 zatoshis.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_update_received_note(
&mut self,
account: AccountId,
diversifier: &Diversifier,
value: u64,
rcm: [u8; 32],
nf: &Option<Nullifier>,
memo: Option<&MemoBytes>,
is_change: Option<bool>,
tx_ref: i64,
output_index: usize,
) -> Result<bool, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":account", &u32::from(account)),
(":diversifier", &diversifier.0.as_ref()),
(":value", &(value as i64)),
(":rcm", &rcm.as_ref()),
(":nf", &nf.as_ref().map(|nf| nf.0.as_ref())),
(":memo", &memo.map(|m| m.as_slice())),
(":is_change", &is_change),
(":tx", &tx_ref),
(":output_index", &(output_index as i64)),
];
match self.stmt_update_received_note.execute_named(sql_args)? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
}
}
/// Finds the database row for the given `txid`, if the transaction is in the wallet.
pub(crate) fn stmt_select_received_note(
&mut self,
tx_ref: i64,
output_index: usize,
) -> Result<NoteId, SqliteClientError> {
self.stmt_select_received_note
.query_row(params![tx_ref, (output_index as i64)], |row| {
row.get(0).map(NoteId::ReceivedNoteId)
})
.map_err(SqliteClientError::from)
}
/// Inserts a sent note into the wallet database.
///
/// `output_index` is the index within the transaction that contains the recipient output:
///
/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the
/// transaction.
/// - If `to` is a transparent address, this is an index into the transparent outputs of
/// the transaction.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_insert_sent_note(
&mut self,
tx_ref: i64,
pool_type: PoolType,
output_index: usize,
account: AccountId,
to_str: &str,
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let ivalue: i64 = value.into();
self.stmt_insert_sent_note.execute(params![
tx_ref,
pool_type.typecode(),
(output_index as i64),
u32::from(account),
to_str,
ivalue,
memo.map(|m| m.as_slice()),
])?;
Ok(())
}
/// Updates the data for the given sent note.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_update_sent_note(
&mut self,
account: AccountId,
to_str: &str,
value: Amount,
memo: Option<&MemoBytes>,
tx_ref: i64,
pool_type: PoolType,
output_index: usize,
) -> Result<bool, SqliteClientError> {
let ivalue: i64 = value.into();
match self.stmt_update_sent_note.execute(params![
u32::from(account),
to_str,
ivalue,
&memo.map(|m| m.as_slice()),
tx_ref,
pool_type.typecode(),
output_index as i64,
])? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
}
}
/// Records the incremental witness for the specified note, as of the given block
/// height.
///
/// Returns `SqliteClientError::InvalidNoteId` if the note ID is for a sent note.
pub(crate) fn stmt_insert_witness(
&mut self,
note_id: NoteId,
height: BlockHeight,
witness: &IncrementalWitness<Node>,
) -> Result<(), SqliteClientError> {
let note_id = match note_id {
NoteId::ReceivedNoteId(note_id) => Ok(note_id),
NoteId::SentNoteId(_) => Err(SqliteClientError::InvalidNoteId),
}?;
let mut encoded = Vec::new();
witness.write(&mut encoded).unwrap();
self.stmt_insert_witness
.execute(params![note_id, u32::from(height), encoded])?;
Ok(())
}
/// Removes old incremental witnesses up to the given block height.
pub(crate) fn stmt_prune_witnesses(
&mut self,
below_height: BlockHeight,
) -> Result<(), SqliteClientError> {
self.stmt_prune_witnesses
.execute(&[u32::from(below_height)])?;
Ok(())
}
/// Marks notes that have not been mined in transactions as expired, up to the given
/// block height.
pub fn stmt_update_expired(&mut self, height: BlockHeight) -> Result<(), SqliteClientError> {
self.stmt_update_expired.execute(&[u32::from(height)])?;
Ok(())
}
}

View File

@ -8,7 +8,7 @@
//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
use ff::PrimeField;
use rusqlite::{params, OptionalExtension, ToSql, NO_PARAMS};
use rusqlite::{OptionalExtension, ToSql, NO_PARAMS};
use std::collections::HashMap;
use std::convert::TryFrom;
@ -38,6 +38,7 @@ use zcash_primitives::legacy::TransparentAddress;
#[cfg(feature = "transparent-inputs")]
use {
crate::UtxoId,
rusqlite::params,
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::{
legacy::Script,
@ -48,13 +49,13 @@ use {
pub mod init;
pub mod transact;
enum PoolType {
pub(crate) enum PoolType {
Transparent,
Sapling,
}
impl PoolType {
fn typecode(&self) -> i64 {
pub(crate) fn typecode(&self) -> i64 {
// These constants are *incidentally* shared with the typecodes
// for unified addresses, but this is exclusively an internal
// implementation detail.
@ -822,17 +823,7 @@ pub fn insert_block<'a, P>(
block_time: u32,
commitment_tree: &CommitmentTree<Node>,
) -> Result<(), SqliteClientError> {
let mut encoded_tree = Vec::new();
commitment_tree.write(&mut encoded_tree).unwrap();
stmts.stmt_insert_block.execute(params![
u32::from(block_height),
&block_hash.0[..],
block_time,
encoded_tree
])?;
Ok(())
stmts.stmt_insert_block(block_height, block_hash, block_time, commitment_tree)
}
/// Inserts information about a mined transaction that was observed to
@ -845,24 +836,12 @@ pub fn put_tx_meta<'a, P, N>(
tx: &WalletTx<N>,
height: BlockHeight,
) -> Result<i64, SqliteClientError> {
let txid = tx.txid.as_ref().to_vec();
if stmts
.stmt_update_tx_meta
.execute(params![u32::from(height), (tx.index as i64), txid])?
== 0
{
if !stmts.stmt_update_tx_meta(height, tx.index, &tx.txid)? {
// It isn't there, so insert our transaction into the database.
stmts
.stmt_insert_tx_meta
.execute(params![txid, u32::from(height), (tx.index as i64),])?;
Ok(stmts.wallet_db.conn.last_insert_rowid())
stmts.stmt_insert_tx_meta(&tx.txid, height, tx.index)
} else {
// It was there, so grab its row number.
stmts
.stmt_select_tx_ref
.query_row(&[txid], |row| row.get(0))
.map_err(SqliteClientError::from)
stmts.stmt_select_tx_ref(&tx.txid)
}
}
@ -875,31 +854,17 @@ pub fn put_tx_data<'a, P>(
tx: &Transaction,
created_at: Option<time::OffsetDateTime>,
) -> Result<i64, SqliteClientError> {
let txid = tx.txid().as_ref().to_vec();
let txid = tx.txid();
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;
if stmts
.stmt_update_tx_data
.execute(params![u32::from(tx.expiry_height()), raw_tx, txid,])?
== 0
{
if !stmts.stmt_update_tx_data(tx.expiry_height(), &raw_tx, &txid)? {
// It isn't there, so insert our transaction into the database.
stmts.stmt_insert_tx_data.execute(params![
txid,
created_at,
u32::from(tx.expiry_height()),
raw_tx
])?;
Ok(stmts.wallet_db.conn.last_insert_rowid())
stmts.stmt_insert_tx_data(&txid, created_at, tx.expiry_height(), &raw_tx)
} else {
// It was there, so grab its row number.
stmts
.stmt_select_tx_ref
.query_row(&[txid], |row| row.get(0))
.map_err(SqliteClientError::from)
stmts.stmt_select_tx_ref(&txid)
}
}
@ -916,9 +881,7 @@ pub fn mark_sapling_note_spent<'a, P>(
tx_ref: i64,
nf: &Nullifier,
) -> Result<(), SqliteClientError> {
stmts
.stmt_mark_sapling_note_spent
.execute(&[tx_ref.to_sql()?, nf.0.to_sql()?])?;
stmts.stmt_mark_sapling_note_spent(tx_ref, nf)?;
Ok(())
}
@ -929,15 +892,7 @@ pub(crate) fn mark_transparent_utxo_spent<'a, P>(
tx_ref: i64,
outpoint: &OutPoint,
) -> Result<(), SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":spent_in_tx", &tx_ref),
(":prevout_txid", &outpoint.hash().to_vec()),
(":prevout_idx", &outpoint.n()),
];
stmts
.stmt_mark_transparent_utxo_spent
.execute_named(sql_args)?;
stmts.stmt_mark_transparent_utxo_spent(tx_ref, outpoint)?;
Ok(())
}
@ -948,23 +903,9 @@ pub(crate) fn put_received_transparent_utxo<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>,
output: &WalletTransparentOutput,
) -> Result<UtxoId, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(
":address",
&output.address().encode(&stmts.wallet_db.params),
),
(":prevout_txid", &output.outpoint.hash().to_vec()),
(":prevout_idx", &output.outpoint.n()),
(":script", &output.txout.script_pubkey.0),
(":value_zat", &i64::from(output.txout.value)),
(":height", &u32::from(output.height)),
];
stmts
.stmt_insert_received_transparent_utxo
.execute_named(sql_args)?;
Ok(UtxoId(stmts.wallet_db.conn.last_insert_rowid()))
.stmt_insert_received_transparent_utxo(output)
.map(UtxoId)
}
/// Removes all records of UTXOs that were recorded as having been received
@ -978,14 +919,7 @@ pub fn delete_utxos_above<'a, P: consensus::Parameters>(
taddr: &TransparentAddress,
height: BlockHeight,
) -> Result<usize, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":address", &taddr.encode(&stmts.wallet_db.params)),
(":above_height", &u32::from(height)),
];
let rows = stmts.stmt_delete_utxos.execute_named(sql_args)?;
Ok(rows)
stmts.stmt_delete_utxos(taddr, height)
}
/// Records the specified shielded output as having been received.
@ -1003,44 +937,41 @@ pub fn put_received_note<'a, P, T: ShieldedOutput>(
tx_ref: i64,
) -> Result<NoteId, SqliteClientError> {
let rcm = output.note().rcm().to_repr();
let account = u32::from(output.account());
let diversifier = output.to().diversifier().0.to_vec();
let value = output.note().value as i64;
let rcm = rcm.as_ref();
let memo = output.memo().map(|m| m.as_slice());
let account = output.account();
let diversifier = output.to().diversifier();
let value = output.note().value;
let memo = output.memo();
let is_change = output.is_change();
let tx = tx_ref;
let output_index = output.index() as i64;
let nf_bytes = output.nullifier().map(|nf| nf.0.to_vec());
let sql_args: &[(&str, &dyn ToSql)] = &[
(":account", &account),
(":diversifier", &diversifier),
(":value", &value),
(":rcm", &rcm),
(":nf", &nf_bytes),
(":memo", &memo),
(":is_change", &is_change),
(":tx", &tx),
(":output_index", &output_index),
];
let output_index = output.index();
let nf = output.nullifier();
// First try updating an existing received note into the database.
if stmts.stmt_update_received_note.execute_named(sql_args)? == 0 {
if !stmts.stmt_update_received_note(
account,
diversifier,
value,
rcm,
&nf,
memo,
is_change,
tx_ref,
output_index,
)? {
// It isn't there, so insert our note into the database.
stmts.stmt_insert_received_note.execute_named(sql_args)?;
Ok(NoteId::ReceivedNoteId(
stmts.wallet_db.conn.last_insert_rowid(),
))
stmts.stmt_insert_received_note(
tx_ref,
output_index,
account,
diversifier,
value,
rcm,
&nf,
memo,
is_change,
)
} else {
// It was there, so grab its row number.
stmts
.stmt_select_received_note
.query_row(params![tx_ref, (output.index() as i64)], |row| {
row.get(0).map(NoteId::ReceivedNoteId)
})
.map_err(SqliteClientError::from)
stmts.stmt_select_received_note(tx_ref, output.index())
}
}
@ -1055,14 +986,7 @@ pub fn insert_witness<'a, P>(
witness: &IncrementalWitness<Node>,
height: BlockHeight,
) -> Result<(), SqliteClientError> {
let mut encoded = Vec::new();
witness.write(&mut encoded).unwrap();
stmts
.stmt_insert_witness
.execute(params![note_id, u32::from(height), encoded])?;
Ok(())
stmts.stmt_insert_witness(NoteId::ReceivedNoteId(note_id), height, witness)
}
/// Removes old incremental witnesses up to the given block height.
@ -1073,10 +997,7 @@ pub fn prune_witnesses<P>(
stmts: &mut DataConnStmtCache<'_, P>,
below_height: BlockHeight,
) -> Result<(), SqliteClientError> {
stmts
.stmt_prune_witnesses
.execute(&[u32::from(below_height)])?;
Ok(())
stmts.stmt_prune_witnesses(below_height)
}
/// Marks notes that have not been mined in transactions
@ -1088,8 +1009,7 @@ pub fn update_expired_notes<P>(
stmts: &mut DataConnStmtCache<'_, P>,
height: BlockHeight,
) -> Result<(), SqliteClientError> {
stmts.stmt_update_expired.execute(&[u32::from(height)])?;
Ok(())
stmts.stmt_update_expired(height)
}
/// Records information about a note that your wallet created.
@ -1106,18 +1026,16 @@ pub fn put_sent_note<'a, P: consensus::Parameters>(
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let ivalue: i64 = value.into();
// Try updating an existing sent note.
if stmts.stmt_update_sent_note.execute(params![
u32::from(account),
encode_payment_address_p(&stmts.wallet_db.params, to),
ivalue,
&memo.map(|m| m.as_slice()),
if !stmts.stmt_update_sent_note(
account,
&encode_payment_address_p(&stmts.wallet_db.params, to),
value,
memo,
tx_ref,
PoolType::Sapling.typecode(),
output_index as i64,
])? == 0
{
PoolType::Sapling,
output_index,
)? {
// It isn't there, so insert.
insert_sent_note(stmts, tx_ref, output_index, account, to, value, memo)?
}
@ -1141,18 +1059,16 @@ pub fn put_sent_utxo<'a, P: consensus::Parameters>(
to: &TransparentAddress,
value: Amount,
) -> Result<(), SqliteClientError> {
let ivalue: i64 = value.into();
// Try updating an existing sent UTXO.
if stmts.stmt_update_sent_note.execute(params![
u32::from(account),
encode_transparent_address_p(&stmts.wallet_db.params, to),
ivalue,
(None::<&[u8]>),
if !stmts.stmt_update_sent_note(
account,
&encode_transparent_address_p(&stmts.wallet_db.params, to),
value,
None,
tx_ref,
PoolType::Transparent.typecode(),
output_index as i64,
])? == 0
{
PoolType::Transparent,
output_index,
)? {
// It isn't there, so insert.
insert_sent_utxo(stmts, tx_ref, output_index, account, to, value)?
}
@ -1181,18 +1097,16 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let to_str = encode_payment_address_p(&stmts.wallet_db.params, to);
let ivalue: i64 = value.into();
stmts.stmt_insert_sent_note.execute(params![
tx_ref,
PoolType::Sapling.typecode(),
(output_index as i64),
u32::from(account),
to_str,
ivalue,
memo.map(|m| m.as_slice().to_vec()),
])?;
Ok(())
stmts.stmt_insert_sent_note(
tx_ref,
PoolType::Sapling,
output_index,
account,
&to_str,
value,
memo,
)
}
/// Inserts information about a sent transparent UTXO into the wallet database.
@ -1210,18 +1124,16 @@ pub fn insert_sent_utxo<'a, P: consensus::Parameters>(
value: Amount,
) -> Result<(), SqliteClientError> {
let to_str = encode_transparent_address_p(&stmts.wallet_db.params, to);
let ivalue: i64 = value.into();
stmts.stmt_insert_sent_note.execute(params![
tx_ref,
PoolType::Transparent.typecode(),
output_index as i64,
u32::from(account),
to_str,
ivalue,
(None::<&[u8]>),
])?;
Ok(())
stmts.stmt_insert_sent_note(
tx_ref,
PoolType::Transparent,
output_index,
account,
&to_str,
value,
None,
)
}
#[cfg(test)]