Add fee to the transactions table & compute correct net_value in v_transactions.

This commit is contained in:
Kris Nuttycombe 2022-09-01 21:03:39 -06:00
parent 66c9f31e14
commit 95610f7b4f
16 changed files with 368 additions and 76 deletions

View File

@ -88,7 +88,8 @@ and this library adheres to Rust's notion of
- The `zcash_client_backend::data_api::SentTransaction` type has been - The `zcash_client_backend::data_api::SentTransaction` type has been
substantially modified to accommodate handling of transparent inputs. substantially modified to accommodate handling of transparent inputs.
Per-output data has been split out into a new struct `SentTransactionOutput` Per-output data has been split out into a new struct `SentTransactionOutput`
and `SentTransaction` can now contain multiple outputs. and `SentTransaction` can now contain multiple outputs, and tracks the
fee paid.
- `data_api::WalletWrite::store_received_tx` has been renamed to - `data_api::WalletWrite::store_received_tx` has been renamed to
`store_decrypted_tx`. `store_decrypted_tx`.
- `data_api::ReceivedTransaction` has been renamed to `DecryptedTransaction`, - `data_api::ReceivedTransaction` has been renamed to `DecryptedTransaction`,

View File

@ -245,6 +245,7 @@ pub struct SentTransaction<'a> {
pub created: time::OffsetDateTime, pub created: time::OffsetDateTime,
pub account: AccountId, pub account: AccountId,
pub outputs: Vec<SentTransactionOutput<'a>>, pub outputs: Vec<SentTransactionOutput<'a>>,
pub fee_amount: Amount,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
pub utxos_spent: Vec<OutPoint>, pub utxos_spent: Vec<OutPoint>,
} }

View File

@ -316,7 +316,7 @@ where
} }
// Create the transaction // Create the transaction
let mut builder = Builder::new(params.clone(), height); let mut builder = Builder::new_with_fee(params.clone(), height, DEFAULT_FEE);
for selected in spendable_notes { for selected in spendable_notes {
let from = extfvk let from = extfvk
.fvk .fvk
@ -400,8 +400,9 @@ where
wallet_db.store_sent_tx(&SentTransaction { wallet_db.store_sent_tx(&SentTransaction {
tx: &tx, tx: &tx,
created: time::OffsetDateTime::now_utc(), created: time::OffsetDateTime::now_utc(),
outputs: sent_outputs,
account, account,
outputs: sent_outputs,
fee_amount: DEFAULT_FEE,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
utxos_spent: vec![], utxos_spent: vec![],
}) })
@ -494,7 +495,7 @@ where
let amount_to_shield = (total_amount - fee).ok_or_else(|| E::from(Error::InvalidAmount))?; let amount_to_shield = (total_amount - fee).ok_or_else(|| E::from(Error::InvalidAmount))?;
let mut builder = Builder::new(params.clone(), latest_scanned_height); let mut builder = Builder::new_with_fee(params.clone(), latest_scanned_height, fee);
let secret_key = sk.derive_external_secret_key(child_index).unwrap(); let secret_key = sk.derive_external_secret_key(child_index).unwrap();
for utxo in &utxos { for utxo in &utxos {
@ -531,6 +532,7 @@ where
value: amount_to_shield, value: amount_to_shield,
memo: Some(memo.clone()), memo: Some(memo.clone()),
}], }],
fee_amount: fee,
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint.clone()).collect(), utxos_spent: utxos.iter().map(|utxo| utxo.outpoint.clone()).collect(),
}) })
} }

View File

@ -33,12 +33,18 @@ zcash_client_backend = { version = "0.5", path = "../zcash_client_backend" }
zcash_primitives = { version = "0.7", path = "../zcash_primitives" } zcash_primitives = { version = "0.7", path = "../zcash_primitives" }
[dev-dependencies] [dev-dependencies]
proptest = "1.0.0"
regex = "1.4"
tempfile = "3" tempfile = "3"
zcash_proofs = { version = "0.7", path = "../zcash_proofs" } zcash_proofs = { version = "0.7", path = "../zcash_proofs" }
zcash_primitives = { version = "0.7", path = "../zcash_primitives", features = ["test-dependencies"] }
[features] [features]
mainnet = [] mainnet = []
test-dependencies = ["zcash_client_backend/test-dependencies"] test-dependencies = [
"zcash_primitives/test-dependencies",
"zcash_client_backend/test-dependencies",
]
transparent-inputs = ["hdwallet", "zcash_client_backend/transparent-inputs"] transparent-inputs = ["hdwallet", "zcash_client_backend/transparent-inputs"]
unstable = ["zcash_client_backend/unstable"] unstable = ["zcash_client_backend/unstable"]

View File

@ -456,7 +456,7 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
) -> Result<Self::TxRef, Self::Error> { ) -> Result<Self::TxRef, Self::Error> {
let nullifiers = self.wallet_db.get_all_nullifiers()?; let nullifiers = self.wallet_db.get_all_nullifiers()?;
self.transactionally(|up| { self.transactionally(|up| {
let tx_ref = wallet::put_tx_data(up, d_tx.tx, None)?; let tx_ref = wallet::put_tx_data(up, d_tx.tx, None, None)?;
let mut spending_account_id: Option<AccountId> = None; let mut spending_account_id: Option<AccountId> = None;
for output in d_tx.sapling_outputs { for output in d_tx.sapling_outputs {
@ -515,7 +515,12 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error> { fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error> {
// Update the database atomically, to ensure the result is internally consistent. // Update the database atomically, to ensure the result is internally consistent.
self.transactionally(|up| { self.transactionally(|up| {
let tx_ref = wallet::put_tx_data(up, sent_tx.tx, Some(sent_tx.created))?; let tx_ref = wallet::put_tx_data(
up,
sent_tx.tx,
Some(sent_tx.fee_amount),
Some(sent_tx.created),
)?;
// Mark notes as spent. // Mark notes as spent.
// //

View File

@ -84,12 +84,15 @@ impl<'a, P> DataConnStmtCache<'a, P> {
SET block = ?, tx_index = ? WHERE txid = ?", SET block = ?, tx_index = ? WHERE txid = ?",
)?, )?,
stmt_insert_tx_data: wallet_db.conn.prepare( stmt_insert_tx_data: wallet_db.conn.prepare(
"INSERT INTO transactions (txid, created, expiry_height, raw) "INSERT INTO transactions (txid, created, expiry_height, raw, fee)
VALUES (?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?)",
)?, )?,
stmt_update_tx_data: wallet_db.conn.prepare( stmt_update_tx_data: wallet_db.conn.prepare(
"UPDATE transactions "UPDATE transactions
SET expiry_height = ?, raw = ? WHERE txid = ?", SET expiry_height = :expiry_height,
raw = :raw,
fee = IFNULL(:fee, fee)
WHERE txid = :txid",
)?, )?,
stmt_select_tx_ref: wallet_db.conn.prepare( stmt_select_tx_ref: wallet_db.conn.prepare(
"SELECT id_tx FROM transactions WHERE txid = ?", "SELECT id_tx FROM transactions WHERE txid = ?",
@ -226,12 +229,14 @@ impl<'a, P> DataConnStmtCache<'a, P> {
created_at: Option<time::OffsetDateTime>, created_at: Option<time::OffsetDateTime>,
expiry_height: BlockHeight, expiry_height: BlockHeight,
raw_tx: &[u8], raw_tx: &[u8],
fee: Option<Amount>,
) -> Result<i64, SqliteClientError> { ) -> Result<i64, SqliteClientError> {
self.stmt_insert_tx_data.execute(params![ self.stmt_insert_tx_data.execute(params![
&txid.as_ref()[..], &txid.as_ref()[..],
created_at, created_at,
u32::from(expiry_height), u32::from(expiry_height),
raw_tx raw_tx,
fee.map(i64::from)
])?; ])?;
Ok(self.wallet_db.conn.last_insert_rowid()) Ok(self.wallet_db.conn.last_insert_rowid())
@ -244,13 +249,16 @@ impl<'a, P> DataConnStmtCache<'a, P> {
&mut self, &mut self,
expiry_height: BlockHeight, expiry_height: BlockHeight,
raw_tx: &[u8], raw_tx: &[u8],
fee: Option<Amount>,
txid: &TxId, txid: &TxId,
) -> Result<bool, SqliteClientError> { ) -> Result<bool, SqliteClientError> {
match self.stmt_update_tx_data.execute(params![ let sql_args: &[(&str, &dyn ToSql)] = &[
u32::from(expiry_height), (":expiry_height", &u32::from(expiry_height)),
raw_tx, (":raw", &raw_tx),
&txid.as_ref()[..], (":fee", &fee.map(i64::from)),
])? { (":txid", &&txid.as_ref()[..]),
];
match self.stmt_update_tx_data.execute_named(sql_args)? {
0 => Ok(false), 0 => Ok(false),
1 => Ok(true), 1 => Ok(true),
_ => unreachable!("txid column is marked as UNIQUE"), _ => unreachable!("txid column is marked as UNIQUE"),

View File

@ -936,6 +936,7 @@ pub fn put_tx_meta<'a, P, N>(
pub fn put_tx_data<'a, P>( pub fn put_tx_data<'a, P>(
stmts: &mut DataConnStmtCache<'a, P>, stmts: &mut DataConnStmtCache<'a, P>,
tx: &Transaction, tx: &Transaction,
fee: Option<Amount>,
created_at: Option<time::OffsetDateTime>, created_at: Option<time::OffsetDateTime>,
) -> Result<i64, SqliteClientError> { ) -> Result<i64, SqliteClientError> {
let txid = tx.txid(); let txid = tx.txid();
@ -943,9 +944,9 @@ pub fn put_tx_data<'a, P>(
let mut raw_tx = vec![]; let mut raw_tx = vec![];
tx.write(&mut raw_tx)?; tx.write(&mut raw_tx)?;
if !stmts.stmt_update_tx_data(tx.expiry_height(), &raw_tx, &txid)? { if !stmts.stmt_update_tx_data(tx.expiry_height(), &raw_tx, fee, &txid)? {
// It isn't there, so insert our transaction into the database. // It isn't there, so insert our transaction into the database.
stmts.stmt_insert_tx_data(&txid, created_at, tx.expiry_height(), &raw_tx) stmts.stmt_insert_tx_data(&txid, created_at, tx.expiry_height(), &raw_tx, fee)
} else { } else {
// It was there, so grab its row number. // It was there, so grab its row number.
stmts.stmt_select_tx_ref(&txid) stmts.stmt_select_tx_ref(&txid)

View File

@ -1,5 +1,5 @@
//! Functions for initializing the various databases. //! Functions for initializing the various databases.
use rusqlite::{self, params, types::ToSql, Connection, Transaction, NO_PARAMS}; use rusqlite::{self, params, types::ToSql, Connection, NO_PARAMS};
use schemer::{migration, Migration, Migrator, MigratorError}; use schemer::{migration, Migration, Migrator, MigratorError};
use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration}; use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration};
use secrecy::{ExposeSecret, SecretVec}; use secrecy::{ExposeSecret, SecretVec};
@ -9,7 +9,11 @@ use uuid::Uuid;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{self, BlockHeight}, consensus::{self, BlockHeight, BranchId},
transaction::{
components::amount::{Amount, BalanceError},
Transaction,
},
zip32::AccountId, zip32::AccountId,
}; };
@ -38,6 +42,9 @@ pub enum WalletMigrationError {
/// Wrapper for rusqlite errors. /// Wrapper for rusqlite errors.
DbError(rusqlite::Error), DbError(rusqlite::Error),
/// Wrapper for amount balance violations
BalanceError(BalanceError),
} }
impl From<rusqlite::Error> for WalletMigrationError { impl From<rusqlite::Error> for WalletMigrationError {
@ -46,6 +53,12 @@ impl From<rusqlite::Error> for WalletMigrationError {
} }
} }
impl From<BalanceError> for WalletMigrationError {
fn from(e: BalanceError) -> Self {
WalletMigrationError::BalanceError(e)
}
}
impl fmt::Display for WalletMigrationError { impl fmt::Display for WalletMigrationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self { match &self {
@ -59,6 +72,7 @@ impl fmt::Display for WalletMigrationError {
write!(f, "Wallet database is corrupted: {}", reason) write!(f, "Wallet database is corrupted: {}", reason)
} }
WalletMigrationError::DbError(e) => write!(f, "{}", e), WalletMigrationError::DbError(e) => write!(f, "{}", e),
WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e),
} }
} }
} }
@ -84,7 +98,7 @@ migration!(
impl RusqliteMigration for WalletMigration0 { impl RusqliteMigration for WalletMigration0 {
type Error = WalletMigrationError; type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch( transaction.execute_batch(
// We set the user_version field of the database to a constant value of 8 to allow // We set the user_version field of the database to a constant value of 8 to allow
// correct integration with the Android SDK with versions of the database that were // correct integration with the Android SDK with versions of the database that were
@ -155,7 +169,7 @@ impl RusqliteMigration for WalletMigration0 {
Ok(()) Ok(())
} }
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// We should never down-migrate the first migration, as that can irreversibly // We should never down-migrate the first migration, as that can irreversibly
// destroy data. // destroy data.
panic!("Cannot revert the initial migration."); panic!("Cannot revert the initial migration.");
@ -174,7 +188,7 @@ migration!(
impl RusqliteMigration for WalletMigration1 { impl RusqliteMigration for WalletMigration1 {
type Error = WalletMigrationError; type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch( transaction.execute_batch(
"CREATE TABLE IF NOT EXISTS utxos ( "CREATE TABLE IF NOT EXISTS utxos (
id_utxo INTEGER PRIMARY KEY, id_utxo INTEGER PRIMARY KEY,
@ -192,18 +206,18 @@ impl RusqliteMigration for WalletMigration1 {
Ok(()) Ok(())
} }
fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch("DROP TABLE utxos;")?; transaction.execute_batch("DROP TABLE utxos;")?;
Ok(()) Ok(())
} }
} }
struct WalletMigration2<P: consensus::Parameters> { struct WalletMigration2<P> {
params: P, params: P,
seed: Option<SecretVec<u8>>, seed: Option<SecretVec<u8>>,
} }
impl<P: consensus::Parameters> Migration for WalletMigration2<P> { impl<P> Migration for WalletMigration2<P> {
fn id(&self) -> Uuid { fn id(&self) -> Uuid {
::uuid::Uuid::parse_str("be57ef3b-388e-42ea-97e2-678dafcf9754").unwrap() ::uuid::Uuid::parse_str("be57ef3b-388e-42ea-97e2-678dafcf9754").unwrap()
} }
@ -223,7 +237,7 @@ impl<P: consensus::Parameters> Migration for WalletMigration2<P> {
impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> { impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
type Error = WalletMigrationError; type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// //
// Update the accounts table to store ufvks rather than extfvks // Update the accounts table to store ufvks rather than extfvks
// //
@ -429,25 +443,77 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
Ok(()) Ok(())
} }
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> { fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic? // TODO: something better than just panic?
panic!("Cannot revert this migration."); panic!("Cannot revert this migration.");
} }
} }
struct WalletMigrationAddTxViews; struct WalletMigrationAddTxViews<P> {
params: P,
}
migration!( impl<P> Migration for WalletMigrationAddTxViews<P> {
WalletMigrationAddTxViews, fn id(&self) -> Uuid {
"282fad2e-8372-4ca0-8bed-71821320909f", ::uuid::Uuid::parse_str("282fad2e-8372-4ca0-8bed-71821320909f").unwrap()
["be57ef3b-388e-42ea-97e2-678dafcf9754"], }
"Add views over transaction & note data."
);
impl RusqliteMigration for WalletMigrationAddTxViews { fn dependencies(&self) -> HashSet<Uuid> {
["be57ef3b-388e-42ea-97e2-678dafcf9754"]
.iter()
.map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap())
.collect()
}
fn description(&self) -> &'static str {
"Add transaction summary views & add fee information to transactions."
}
}
impl<P: consensus::Parameters> RusqliteMigration for WalletMigrationAddTxViews<P> {
type Error = WalletMigrationError; type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch("ALTER TABLE transactions ADD COLUMN fee INTEGER;")?;
let mut stmt_list_txs =
transaction.prepare("SELECT id_tx, raw, block FROM transactions")?;
let mut stmt_set_fee =
transaction.prepare("UPDATE transactions SET fee = ? WHERE id_tx = ?")?;
let mut stmt_find_utxo_value = transaction
.prepare("SELECT value_zat FROM utxos WHERE prevout_txid = ? AND prevout_idx = ?")?;
let mut tx_rows = stmt_list_txs.query(NO_PARAMS)?;
while let Some(row) = tx_rows.next()? {
let id_tx: i64 = row.get(0)?;
let tx_bytes: Vec<u8> = row.get(1)?;
let h: u32 = row.get(2)?;
let block_height = BlockHeight::from(h);
let tx = Transaction::read(
&tx_bytes[..],
BranchId::for_height(&self.params, block_height),
)
.map_err(|e| {
WalletMigrationError::CorruptedData(format!(
"Parsing failed for transaction {:?}: {:?}",
id_tx, e
))
})?;
let fee_paid = tx.fee_paid(|op| {
stmt_find_utxo_value
.query_row(&[op.hash().to_sql()?, op.n().to_sql()?], |row| {
row.get(0).map(|i| Amount::from_i64(i).unwrap())
})
.map_err(WalletMigrationError::DbError)
})?;
stmt_set_fee.execute(&[i64::from(fee_paid), id_tx])?;
}
transaction.execute_batch( transaction.execute_batch(
"CREATE VIEW v_tx_sent AS "CREATE VIEW v_tx_sent AS
SELECT transactions.id_tx AS id_tx, SELECT transactions.id_tx AS id_tx,
@ -488,7 +554,7 @@ impl RusqliteMigration for WalletMigrationAddTxViews {
txid, txid,
expiry_height, expiry_height,
raw, raw,
SUM(value) AS net_value, SUM(value) + MAX(fee) AS net_value,
SUM(is_change) > 0 AS has_change, SUM(is_change) > 0 AS has_change,
SUM(memo_present) AS memo_count SUM(memo_present) AS memo_count
FROM ( FROM (
@ -498,7 +564,11 @@ impl RusqliteMigration for WalletMigrationAddTxViews {
transactions.txid AS txid, transactions.txid AS txid,
transactions.expiry_height AS expiry_height, transactions.expiry_height AS expiry_height,
transactions.raw AS raw, transactions.raw AS raw,
received_notes.value AS value, transactions.fee AS fee,
CASE
WHEN received_notes.is_change THEN 0
ELSE value
END AS value,
received_notes.is_change AS is_change, received_notes.is_change AS is_change,
CASE WHEN received_notes.memo IS NULL OR received_notes.memo = '' THEN 0 ELSE 1 END AS memo_present CASE WHEN received_notes.memo IS NULL OR received_notes.memo = '' THEN 0 ELSE 1 END AS memo_present
FROM transactions FROM transactions
@ -510,6 +580,7 @@ impl RusqliteMigration for WalletMigrationAddTxViews {
transactions.txid AS txid, transactions.txid AS txid,
transactions.expiry_height AS expiry_height, transactions.expiry_height AS expiry_height,
transactions.raw AS raw, transactions.raw AS raw,
0 AS fee,
-sent_notes.value AS value, -sent_notes.value AS value,
false AS is_change, false AS is_change,
CASE WHEN sent_notes.memo IS NULL OR sent_notes.memo = '' THEN 0 ELSE 1 END AS memo_present CASE WHEN sent_notes.memo IS NULL OR sent_notes.memo = '' THEN 0 ELSE 1 END AS memo_present
@ -522,7 +593,7 @@ impl RusqliteMigration for WalletMigrationAddTxViews {
Ok(()) Ok(())
} }
fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch( transaction.execute_batch(
"DROP VIEW v_tx_sent_notes; "DROP VIEW v_tx_sent_notes;
DROP VIEW v_tx_received_notes; DROP VIEW v_tx_received_notes;
@ -584,7 +655,9 @@ pub fn init_wallet_db<P: consensus::Parameters + 'static>(
params: wdb.params.clone(), params: wdb.params.clone(),
seed, seed,
}); });
let migration3 = Box::new(WalletMigrationAddTxViews {}); let migration3 = Box::new(WalletMigrationAddTxViews {
params: wdb.params.clone(),
});
let migration4 = Box::new(migrations::AddressesTableMigration { let migration4 = Box::new(migrations::AddressesTableMigration {
params: wdb.params.clone(), params: wdb.params.clone(),
}); });
@ -782,8 +855,9 @@ mod tests {
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{BlockHeight, Parameters}, consensus::{BlockHeight, BranchId, Parameters},
sapling::keys::DiversifiableFullViewingKey, sapling::keys::DiversifiableFullViewingKey,
transaction::{TransactionData, TxVersion},
zip32::ExtendedFullViewingKey, zip32::ExtendedFullViewingKey,
}; };
@ -804,6 +878,9 @@ mod tests {
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, None).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
use regex::Regex;
let re = Regex::new(r"\s+").unwrap();
let expected_tables = vec![ let expected_tables = vec![
"CREATE TABLE \"accounts\" ( "CREATE TABLE \"accounts\" (
account INTEGER PRIMARY KEY, account INTEGER PRIMARY KEY,
@ -849,8 +926,8 @@ mod tests {
CONSTRAINT witness_height UNIQUE (note, block) CONSTRAINT witness_height UNIQUE (note, block)
)", )",
"CREATE TABLE schemer_migrations ( "CREATE TABLE schemer_migrations (
id blob PRIMARY KEY id blob PRIMARY KEY
)", )",
"CREATE TABLE \"sent_notes\" ( "CREATE TABLE \"sent_notes\" (
id_note INTEGER PRIMARY KEY, id_note INTEGER PRIMARY KEY,
tx INTEGER NOT NULL, tx INTEGER NOT NULL,
@ -872,6 +949,7 @@ mod tests {
tx_index INTEGER, tx_index INTEGER,
expiry_height INTEGER, expiry_height INTEGER,
raw BLOB, raw BLOB,
fee INTEGER,
FOREIGN KEY (block) REFERENCES blocks(height) FOREIGN KEY (block) REFERENCES blocks(height)
)", )",
"CREATE TABLE utxos ( "CREATE TABLE utxos (
@ -896,7 +974,10 @@ mod tests {
let mut expected_idx = 0; let mut expected_idx = 0;
while let Some(row) = rows.next().unwrap() { while let Some(row) = rows.next().unwrap() {
let sql: String = row.get(0).unwrap(); let sql: String = row.get(0).unwrap();
assert_eq!(&sql, expected_tables[expected_idx]); assert_eq!(
re.replace_all(&sql, " "),
re.replace_all(expected_tables[expected_idx], " ")
);
expected_idx += 1; expected_idx += 1;
} }
@ -908,7 +989,7 @@ mod tests {
txid, txid,
expiry_height, expiry_height,
raw, raw,
SUM(value) AS net_value, SUM(value) + MAX(fee) AS net_value,
SUM(is_change) > 0 AS has_change, SUM(is_change) > 0 AS has_change,
SUM(memo_present) AS memo_count SUM(memo_present) AS memo_count
FROM ( FROM (
@ -918,7 +999,11 @@ mod tests {
transactions.txid AS txid, transactions.txid AS txid,
transactions.expiry_height AS expiry_height, transactions.expiry_height AS expiry_height,
transactions.raw AS raw, transactions.raw AS raw,
received_notes.value AS value, transactions.fee AS fee,
CASE
WHEN received_notes.is_change THEN 0
ELSE value
END AS value,
received_notes.is_change AS is_change, received_notes.is_change AS is_change,
CASE WHEN received_notes.memo IS NULL OR received_notes.memo = '' THEN 0 ELSE 1 END AS memo_present CASE WHEN received_notes.memo IS NULL OR received_notes.memo = '' THEN 0 ELSE 1 END AS memo_present
FROM transactions FROM transactions
@ -930,6 +1015,7 @@ mod tests {
transactions.txid AS txid, transactions.txid AS txid,
transactions.expiry_height AS expiry_height, transactions.expiry_height AS expiry_height,
transactions.raw AS raw, transactions.raw AS raw,
0 AS fee,
-sent_notes.value AS value, -sent_notes.value AS value,
false AS is_change, false AS is_change,
CASE WHEN sent_notes.memo IS NULL OR sent_notes.memo = '' THEN 0 ELSE 1 END AS memo_present CASE WHEN sent_notes.memo IS NULL OR sent_notes.memo = '' THEN 0 ELSE 1 END AS memo_present
@ -979,7 +1065,10 @@ mod tests {
let mut expected_idx = 0; let mut expected_idx = 0;
while let Some(row) = rows.next().unwrap() { while let Some(row) = rows.next().unwrap() {
let sql: String = row.get(0).unwrap(); let sql: String = row.get(0).unwrap();
assert_eq!(&sql, expected_views[expected_idx]); assert_eq!(
re.replace_all(&sql, " "),
re.replace_all(expected_views[expected_idx], " ")
);
expected_idx += 1; expected_idx += 1;
} }
} }
@ -1224,9 +1313,25 @@ mod tests {
"INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')", "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')",
NO_PARAMS, NO_PARAMS,
)?; )?;
let tx = TransactionData::from_parts(
TxVersion::Sapling,
BranchId::Canopy,
0,
BlockHeight::from(0),
None,
None,
None,
None,
)
.freeze()
.unwrap();
let mut tx_bytes = vec![];
tx.write(&mut tx_bytes).unwrap();
wdb.conn.execute( wdb.conn.execute(
"INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')", "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, '', ?)",
NO_PARAMS, &[&tx_bytes[..]],
)?; )?;
wdb.conn.execute( wdb.conn.execute(
"INSERT INTO sent_notes (tx, output_index, from_account, address, value) "INSERT INTO sent_notes (tx, output_index, from_account, address, value)
@ -1461,7 +1566,7 @@ mod tests {
let net_value: i64 = row.get(0).unwrap(); let net_value: i64 = row.get(0).unwrap();
let has_change: bool = row.get(1).unwrap(); let has_change: bool = row.get(1).unwrap();
let memo_count: i64 = row.get(2).unwrap(); let memo_count: i64 = row.get(2).unwrap();
assert_eq!(net_value, 7); assert_eq!(net_value, 0);
assert!(has_change); assert!(has_change);
assert_eq!(memo_count, 3); assert_eq!(memo_count, 3);
} }

View File

@ -685,7 +685,7 @@ mod tests {
precondition: tze::Precondition::from(0, &Precondition::open(hash_1)), precondition: tze::Precondition::from(0, &Precondition::open(hash_1)),
}; };
let tx_a = TransactionData::from_parts( let tx_a = TransactionData::from_parts_zfuture(
TxVersion::ZFuture, TxVersion::ZFuture,
BranchId::ZFuture, BranchId::ZFuture,
0, 0,
@ -716,7 +716,7 @@ mod tests {
precondition: tze::Precondition::from(0, &Precondition::close(hash_2)), precondition: tze::Precondition::from(0, &Precondition::close(hash_2)),
}; };
let tx_b = TransactionData::from_parts( let tx_b = TransactionData::from_parts_zfuture(
TxVersion::ZFuture, TxVersion::ZFuture,
BranchId::ZFuture, BranchId::ZFuture,
0, 0,
@ -743,7 +743,7 @@ mod tests {
witness: tze::Witness::from(0, &Witness::close(preimage_2)), witness: tze::Witness::from(0, &Witness::close(preimage_2)),
}; };
let tx_c = TransactionData::from_parts( let tx_c = TransactionData::from_parts_zfuture(
TxVersion::ZFuture, TxVersion::ZFuture,
BranchId::ZFuture, BranchId::ZFuture,
0, 0,

View File

@ -23,6 +23,12 @@ and this library adheres to Rust's notion of
- `DiversifierIndex::{as_bytes}` - `DiversifierIndex::{as_bytes}`
- `ExtendedSpendingKey::{from_bytes, to_bytes}` - `ExtendedSpendingKey::{from_bytes, to_bytes}`
- Implementations of `From<u32>` and `From<u64>` for `DiversifierIndex` - Implementations of `From<u32>` and `From<u64>` for `DiversifierIndex`
- `zcash_primitives::transaction::Builder` constructors:
- `Builder::new_with_fee`
- `Builder::new_with_rng_and_fee`
- `zcash_primitives::transaction::TransactionData::fee_paid`
- `zcash_primitives::transaction::components::amount::BalanceError`
- `zcash_primitives::transaction::components::sprout::Bundle::value_balance`
### Changed ### Changed
- `zcash_primitives::sapling::ViewingKey` now stores `nk` as a - `zcash_primitives::sapling::ViewingKey` now stores `nk` as a

View File

@ -141,6 +141,18 @@ impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> {
pub fn new(params: P, target_height: BlockHeight) -> Self { pub fn new(params: P, target_height: BlockHeight) -> Self {
Builder::new_with_rng(params, target_height, OsRng) Builder::new_with_rng(params, target_height, OsRng)
} }
/// Creates a new `Builder` targeted for inclusion in the block with the given height, using
/// the specified fee, and otherwise default values for general transaction fields and the
/// default OS random.
///
/// # Default values
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks).
pub fn new_with_fee(params: P, target_height: BlockHeight, fee: Amount) -> Self {
Builder::new_with_rng_and_fee(params, OsRng, target_height, fee)
}
} }
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> { impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
@ -154,7 +166,24 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
/// ///
/// The fee will be set to the default fee (0.0001 ZEC). /// The fee will be set to the default fee (0.0001 ZEC).
pub fn new_with_rng(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> { pub fn new_with_rng(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> {
Self::new_internal(params, target_height, rng) Self::new_internal(params, rng, target_height, DEFAULT_FEE)
}
/// Creates a new `Builder` targeted for inclusion in the block with the given height, and
/// randomness source, using the specified fee, and otherwise default values for general
/// transaction fields and the default OS random.
///
/// # Default values
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks).
pub fn new_with_rng_and_fee(
params: P,
rng: R,
target_height: BlockHeight,
fee: Amount,
) -> Builder<'a, P, R> {
Self::new_internal(params, rng, target_height, fee)
} }
} }
@ -163,13 +192,18 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
/// ///
/// WARNING: THIS MUST REMAIN PRIVATE AS IT ALLOWS CONSTRUCTION /// WARNING: THIS MUST REMAIN PRIVATE AS IT ALLOWS CONSTRUCTION
/// OF BUILDERS WITH NON-CryptoRng RNGs /// OF BUILDERS WITH NON-CryptoRng RNGs
fn new_internal(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> { fn new_internal(
params: P,
rng: R,
target_height: BlockHeight,
fee: Amount,
) -> Builder<'a, P, R> {
Builder { Builder {
params: params.clone(), params: params.clone(),
rng, rng,
target_height, target_height,
expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA, expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
fee: DEFAULT_FEE, fee,
transparent_builder: TransparentBuilder::empty(), transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(params, target_height), sapling_builder: SaplingBuilder::new(params, target_height),
change_address: None, change_address: None,
@ -454,7 +488,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
/// ///
/// WARNING: DO NOT USE IN PRODUCTION /// WARNING: DO NOT USE IN PRODUCTION
pub fn test_only_new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> { pub fn test_only_new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> {
Self::new_internal(params, height, rng) Self::new_internal(params, rng, height, DEFAULT_FEE)
} }
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> { pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> {

View File

@ -222,6 +222,14 @@ impl TryFrom<orchard::ValueSum> for Amount {
} }
} }
/// A type for balance violations in amount addition and subtraction
/// (overflow and underflow of allowed ranges)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BalanceError {
Overflow,
Underflow,
}
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::prelude::prop_compose; use proptest::prelude::prop_compose;

View File

@ -17,6 +17,20 @@ pub struct Bundle {
pub joinsplit_sig: [u8; 64], pub joinsplit_sig: [u8; 64],
} }
impl Bundle {
/// The value balance for the bundle. When this is positive,
/// its value is added to the transparent value pool; when it
/// is negative, its value is subtracted from the transparent
/// value pool.
pub fn value_balance(&self) -> Option<Amount> {
self.joinsplits
.iter()
.try_fold(Amount::zero(), |total, js| {
js.value_balance().and_then(|b| total + b)
})
}
}
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub(crate) enum SproutProof { pub(crate) enum SproutProof {
@ -172,4 +186,12 @@ impl JsDescription {
writer.write_all(&self.ciphertexts[0])?; writer.write_all(&self.ciphertexts[0])?;
writer.write_all(&self.ciphertexts[1]) writer.write_all(&self.ciphertexts[1])
} }
/// The value balance for the JoinSplit. When this is positive,
/// its value is added to the transparent value pool; when it
/// is negative, its value is subtracted from the transparent
/// value pool.
pub fn value_balance(&self) -> Option<Amount> {
self.vpub_new - self.vpub_old
}
} }

View File

@ -7,7 +7,7 @@ use std::io::{self, Read, Write};
use crate::legacy::Script; use crate::legacy::Script;
use super::amount::Amount; use super::amount::{Amount, BalanceError};
pub mod builder; pub mod builder;
@ -62,6 +62,33 @@ impl<A: Authorization> Bundle<A> {
authorization: f.map_authorization(self.authorization), authorization: f.map_authorization(self.authorization),
} }
} }
/// The amount of value added to or removed from the transparent pool by the action of this
/// bundle. A positive value represents that the containing transaction has funds being
/// transferred out of the transparent pool into shielded pools or to fees; a negative value
/// means that the containing transaction has funds being transferred into the transparent pool
/// from the shielded pools.
pub fn value_balance<E, F: FnMut(&OutPoint) -> Result<Amount, E>>(
&self,
mut get_prevout_value: F,
) -> Result<Amount, E>
where
E: From<BalanceError>,
{
let input_sum = self.vin.iter().try_fold(Amount::zero(), |total, txin| {
get_prevout_value(&txin.prevout)
.and_then(|v| (total + v).ok_or_else(|| BalanceError::Overflow.into()))
})?;
let output_sum = self
.vout
.iter()
.map(|p| p.value)
.sum::<Option<Amount>>()
.ok_or(BalanceError::Overflow)?;
(input_sum - output_sum).ok_or_else(|| BalanceError::Underflow.into())
}
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@ -27,13 +27,14 @@ use crate::{
use self::{ use self::{
components::{ components::{
amount::Amount, amount::{Amount, BalanceError},
orchard as orchard_serialization, orchard as orchard_serialization,
sapling::{ sapling::{
self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5, self, OutputDescription, OutputDescriptionV5, SpendDescription, SpendDescriptionV5,
}, },
sprout::{self, JsDescription}, sprout::{self, JsDescription},
transparent::{self, TxIn, TxOut}, transparent::{self, TxIn, TxOut},
OutPoint,
}, },
txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester}, txid::{to_txid, BlockTxCommitmentDigester, TxIdDigester},
util::sha256d::{HashReader, HashWriter}, util::sha256d::{HashReader, HashWriter},
@ -317,7 +318,6 @@ impl<A: Authorization> TransactionData<A> {
sprout_bundle: Option<sprout::Bundle>, sprout_bundle: Option<sprout::Bundle>,
sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>, sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>, orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>,
#[cfg(feature = "zfuture")] tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
) -> Self { ) -> Self {
TransactionData { TransactionData {
version, version,
@ -329,6 +329,32 @@ impl<A: Authorization> TransactionData<A> {
sapling_bundle, sapling_bundle,
orchard_bundle, orchard_bundle,
#[cfg(feature = "zfuture")] #[cfg(feature = "zfuture")]
tze_bundle: None,
}
}
#[cfg(feature = "zfuture")]
#[allow(clippy::too_many_arguments)]
pub fn from_parts_zfuture(
version: TxVersion,
consensus_branch_id: BranchId,
lock_time: u32,
expiry_height: BlockHeight,
transparent_bundle: Option<transparent::Bundle<A::TransparentAuth>>,
sprout_bundle: Option<sprout::Bundle>,
sapling_bundle: Option<sapling::Bundle<A::SaplingAuth>>,
orchard_bundle: Option<orchard::Bundle<A::OrchardAuth, Amount>>,
tze_bundle: Option<tze::Bundle<A::TzeAuth>>,
) -> Self {
TransactionData {
version,
consensus_branch_id,
lock_time,
expiry_height,
transparent_bundle,
sprout_bundle,
sapling_bundle,
orchard_bundle,
tze_bundle, tze_bundle,
} }
} }
@ -370,6 +396,37 @@ impl<A: Authorization> TransactionData<A> {
self.tze_bundle.as_ref() self.tze_bundle.as_ref()
} }
/// Returns the total fees paid by the transaction, given a function that can
/// be used to retrieve the output values of previous transactions' outputs
/// that are being spent in this transaction.
pub fn fee_paid<E, F: FnMut(&OutPoint) -> Result<Amount, E>>(
&self,
get_prevout: F,
) -> Result<Amount, E>
where
E: From<BalanceError>,
{
let value_balances = [
self.transparent_bundle
.as_ref()
.map_or_else(|| Ok(Amount::zero()), |b| b.value_balance(get_prevout))?,
self.sprout_bundle.as_ref().map_or_else(
|| Ok(Amount::zero()),
|b| b.value_balance().ok_or(BalanceError::Overflow),
)?,
self.sapling_bundle
.as_ref()
.map_or_else(Amount::zero, |b| b.value_balance),
self.orchard_bundle
.as_ref()
.map_or_else(Amount::zero, |b| *b.value_balance()),
];
IntoIterator::into_iter(&value_balances)
.sum::<Option<_>>()
.ok_or_else(|| BalanceError::Overflow.into())
}
pub fn digest<D: TransactionDigest<A>>(&self, digester: D) -> D::Digest { pub fn digest<D: TransactionDigest<A>>(&self, digester: D) -> D::Digest {
digester.combine( digester.combine(
digester.digest_header( digester.digest_header(

View File

@ -245,21 +245,30 @@ fn zip_0244() {
}, },
}); });
( #[cfg(not(feature = "zfuture"))]
TransactionData::from_parts( let tdata = TransactionData::from_parts(
txdata.version(), txdata.version(),
txdata.consensus_branch_id(), txdata.consensus_branch_id(),
txdata.lock_time(), txdata.lock_time(),
txdata.expiry_height(), txdata.expiry_height(),
test_bundle, test_bundle,
txdata.sprout_bundle().cloned(), txdata.sprout_bundle().cloned(),
txdata.sapling_bundle().cloned(), txdata.sapling_bundle().cloned(),
txdata.orchard_bundle().cloned(), txdata.orchard_bundle().cloned(),
#[cfg(feature = "zfuture")] );
txdata.tze_bundle().cloned(), #[cfg(feature = "zfuture")]
), let tdata = TransactionData::from_parts_zfuture(
txdata.digest(TxIdDigester), txdata.version(),
) txdata.consensus_branch_id(),
txdata.lock_time(),
txdata.expiry_height(),
test_bundle,
txdata.sprout_bundle().cloned(),
txdata.sapling_bundle().cloned(),
txdata.orchard_bundle().cloned(),
txdata.tze_bundle().cloned(),
);
(tdata, txdata.digest(TxIdDigester))
} }
for tv in self::data::zip_0244::make_test_vectors() { for tv in self::data::zip_0244::make_test_vectors() {