From 7cc25b7e6c48267ca476f36143e2b4a0964cec20 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Mon, 15 Apr 2019 19:03:55 +0300 Subject: [PATCH] unify fee calculation --- miner/src/fee.rs | 58 +++---------- verification/src/accept_block.rs | 71 ++-------------- verification/src/accept_transaction.rs | 26 +----- verification/src/error.rs | 23 ++++++ verification/src/fee.rs | 110 +++++++++++++++++++++++++ verification/src/lib.rs | 4 +- 6 files changed, 157 insertions(+), 135 deletions(-) create mode 100644 verification/src/fee.rs diff --git a/miner/src/fee.rs b/miner/src/fee.rs index 66278935..1fc8d172 100644 --- a/miner/src/fee.rs +++ b/miner/src/fee.rs @@ -1,6 +1,7 @@ use chain::Transaction; use ser::Serializable; use storage::{TransactionOutputProvider, DuplexTransactionOutputProvider}; +use verification::checked_transaction_fee; use MemoryPool; /// Transaction fee calculator for memory pool @@ -32,43 +33,12 @@ impl MemoryPoolFeeCalculator for NonZeroFeeCalculator { } } -/// Compute miner fee for given transaction. +/// Compute miner fee for given (memory pool) transaction. /// -/// It could return a wrong value (that should have overflow/underflow) if either outputs sum, -/// inputs sum or their difference overflows/underflows. But since it is used for prioritizing -/// verified transactions && verification checks that values are correct, the call is safe. -pub fn transaction_fee(store: &TransactionOutputProvider, transaction: &Transaction) -> u64 { - let mut inputs_sum = transaction.inputs.iter().map(|input| - store.transaction_output(&input.previous_output, ::std::usize::MAX) - .expect("transaction must be verified by caller") - .value) - .fold(0u64, |acc, value| acc.saturating_add(value)); - if let Some(ref join_split) = transaction.join_split { - let js_value_pub_new = join_split.descriptions.iter() - .fold(0u64, |acc, jsd| acc.saturating_add(jsd.value_pub_new)); - inputs_sum = inputs_sum.saturating_add(js_value_pub_new); - } - if let Some(ref sapling) = transaction.sapling { - if sapling.balancing_value > 0 { - inputs_sum = inputs_sum.saturating_add(sapling.balancing_value as u64); - } - } - - let mut outputs_sum = transaction.outputs.iter().map(|output| output.value) - .fold(0u64, |acc, value| acc.saturating_add(value)); - if let Some(ref join_split) = transaction.join_split { - let js_value_pub_old = join_split.descriptions.iter() - .fold(0u64, |acc, jsd| acc.saturating_add(jsd.value_pub_old)); - outputs_sum = outputs_sum.saturating_add(js_value_pub_old); - } - if let Some(ref sapling) = transaction.sapling { - if sapling.balancing_value < 0 { - inputs_sum = inputs_sum.saturating_add(sapling.balancing_value - .checked_neg().unwrap_or(::std::i64::MAX) as u64); - } - } - - inputs_sum.saturating_sub(outputs_sum) +/// If any error occurs during computation, zero fee is returned. Normally, zero fee +/// transactions are not accepted to the memory pool. +pub fn transaction_fee(store: &TransactionOutputProvider, tx: &Transaction) -> u64 { + checked_transaction_fee(store, ::std::usize::MAX, tx).unwrap_or(0) } pub fn transaction_fee_rate(store: &TransactionOutputProvider, tx: &Transaction) -> u64 { @@ -77,15 +47,13 @@ pub fn transaction_fee_rate(store: &TransactionOutputProvider, tx: &Transaction) #[cfg(test)] mod tests { - extern crate test_data; - use std::sync::Arc; - use storage::{AsSubstore}; + use storage::AsSubstore; use db::BlockChainDatabase; - use super::*; + use super::transaction_fee_rate; #[test] - fn test_transaction_fee() { + fn transaction_fee_rate_works() { let b0 = test_data::block_builder().header().nonce(1.into()).build() .transaction() .output().value(1_000_000).build() @@ -104,11 +72,9 @@ mod tests { let tx2 = b1.transactions[0].clone(); let db = Arc::new(BlockChainDatabase::init_test_chain(vec![b0.into(), b1.into()])); + let store = db.as_transaction_output_provider(); - assert_eq!(transaction_fee(db.as_transaction_output_provider(), &tx0), 0); - assert_eq!(transaction_fee(db.as_transaction_output_provider(), &tx2), 500_000); - - assert_eq!(transaction_fee_rate(db.as_transaction_output_provider(), &tx0), 0); - assert_eq!(transaction_fee_rate(db.as_transaction_output_provider(), &tx2), 4_901); + assert_eq!(transaction_fee_rate(store, &tx0), 0); + assert_eq!(transaction_fee_rate(store, &tx2), 4_901); } } diff --git a/verification/src/accept_block.rs b/verification/src/accept_block.rs index cab6db67..e9aaf9b2 100644 --- a/verification/src/accept_block.rs +++ b/verification/src/accept_block.rs @@ -6,8 +6,9 @@ use script::{self, Builder}; use sigops::transaction_sigops; use deployments::BlockDeployments; use canon::CanonBlock; -use error::{Error, TransactionError}; +use error::Error; use timestamp::median_timestamp; +use fee::checked_transaction_fee; /// Flexible verification of ordered block pub struct BlockAcceptor<'a> { @@ -174,73 +175,11 @@ impl<'a> BlockCoinbaseMinerReward<'a> { let mut fees: u64 = 0; for (tx_idx, tx) in self.block.transactions.iter().enumerate().skip(1) { - // (1) Total sum of all referenced outputs - let mut incoming: u64 = 0; - for input in tx.raw.inputs.iter() { - let prevout = store.transaction_output(&input.previous_output, tx_idx); - let (sum, overflow) = incoming.overflowing_add(prevout.map(|o| o.value).unwrap_or(0)); - if overflow { - return Err(Error::Transaction(tx_idx, TransactionError::InputValueOverflow)); - } - incoming = sum; - } - - let join_split_public_new = tx.raw.join_split.iter() - .flat_map(|js| &js.descriptions) - .map(|d| d.value_pub_new) - .sum::(); - - incoming = match incoming.overflowing_add(join_split_public_new) { - (_, true) => return Err(Error::Transaction(tx_idx, TransactionError::InputValueOverflow)), - (incoming, _) => incoming, - }; - - if let Some(ref sapling) = tx.raw.sapling { - if sapling.balancing_value > 0 { - let balancing_value = sapling.balancing_value as u64; - - incoming = match incoming.overflowing_add(balancing_value) { - (_, true) => return Err(Error::Transaction(tx_idx, TransactionError::InputValueOverflow)), - (incoming, _) => incoming, - }; - } - } - - // (2) Total sum of all outputs - let mut spends = tx.raw.total_spends(); - - let join_split_public_old = tx.raw.join_split.iter() - .flat_map(|js| &js.descriptions) - .map(|d| d.value_pub_old) - .sum::(); - - spends = match spends.overflowing_add(join_split_public_old) { - (_, true) => return Err(Error::Transaction(tx_idx, TransactionError::OutputValueOverflow)), - (spends, _) => spends, - }; - - if let Some(ref sapling) = tx.raw.sapling { - if sapling.balancing_value < 0 { - let balancing_value = match sapling.balancing_value.checked_neg() { - Some(balancing_value) => balancing_value as u64, - None => return Err(Error::Transaction(tx_idx, TransactionError::OutputValueOverflow)), - }; - - spends = match spends.overflowing_add(balancing_value) { - (_, true) => return Err(Error::Transaction(tx_idx, TransactionError::OutputValueOverflow)), - (spends, _) => spends, - }; - } - } - - // Difference between (1) and (2) - let (difference, overflow) = incoming.overflowing_sub(spends); - if overflow { - return Err(Error::Transaction(tx_idx, TransactionError::Overspend)); - } + let tx_fee = checked_transaction_fee(&store, tx_idx, &tx.raw) + .map_err(|fee_err| Error::Transaction(tx_idx, fee_err.into()))?; // Adding to total fees (with possible overflow) - let (sum, overflow) = fees.overflowing_add(difference); + let (sum, overflow) = fees.overflowing_add(tx_fee); if overflow { return Err(Error::TransactionFeesOverflow) } diff --git a/verification/src/accept_transaction.rs b/verification/src/accept_transaction.rs index 71daf77a..4d0eb06f 100644 --- a/verification/src/accept_transaction.rs +++ b/verification/src/accept_transaction.rs @@ -12,7 +12,7 @@ use chain::{OVERWINTER_TX_VERSION, SAPLING_TX_VERSION, OVERWINTER_TX_VERSION_GRO use constants::COINBASE_MATURITY; use error::TransactionError; use primitives::hash::H256; -use VerificationLevel; +use {checked_transaction_fee, VerificationLevel}; use tree_cache::TreeCache; pub struct TransactionAcceptor<'a> { @@ -22,7 +22,6 @@ pub struct TransactionAcceptor<'a> { pub bip30: TransactionBip30<'a>, pub missing_inputs: TransactionMissingInputs<'a>, pub maturity: TransactionMaturity<'a>, - pub overspent: TransactionOverspent<'a>, pub double_spent: TransactionDoubleSpend<'a>, pub eval: TransactionEval<'a>, pub join_split: JoinSplitVerification<'a>, @@ -54,7 +53,6 @@ impl<'a> TransactionAcceptor<'a> { bip30: TransactionBip30::new_for_sync(transaction, meta_store), missing_inputs: TransactionMissingInputs::new(transaction, output_store, transaction_index), maturity: TransactionMaturity::new(transaction, meta_store, height), - overspent: TransactionOverspent::new(transaction, output_store), double_spent: TransactionDoubleSpend::new(transaction, output_store), eval: TransactionEval::new(transaction, output_store, consensus, verification_level, height, time, deployments), join_split: JoinSplitVerification::new(consensus, transaction, nullifier_tracker, tree_state_provider), @@ -74,7 +72,6 @@ impl<'a> TransactionAcceptor<'a> { self.bip30.check()?; self.missing_inputs.check()?; self.maturity.check()?; - self.overspent.check()?; self.double_spent.check()?; // to make sure we're using the sighash-cache, let's make all sighash-related @@ -274,24 +271,9 @@ impl<'a> TransactionOverspent<'a> { return Ok(()); } - let available_public = self.transaction.raw.inputs.iter() - .map(|input| self.store.transaction_output(&input.previous_output, usize::max_value()).map(|o| o.value).unwrap_or(0)) - .sum::(); - - let available_join_split = self.transaction.raw.join_split.iter() - .flat_map(|js| &js.descriptions) - .map(|d| d.value_pub_new) - .sum::(); - - let total_available = available_public + available_join_split; - - let spends = self.transaction.raw.total_spends(); - - if spends > total_available { - Err(TransactionError::Overspend) - } else { - Ok(()) - } + checked_transaction_fee(&self.store, ::std::usize::MAX, &self.transaction.raw) + .map_err(Into::into) + .map(|_| ()) } } diff --git a/verification/src/error.rs b/verification/src/error.rs index 97fa03fd..091575c0 100644 --- a/verification/src/error.rs +++ b/verification/src/error.rs @@ -154,3 +154,26 @@ pub enum TransactionError { UnknownAnchor(H256), } +#[derive(Debug, PartialEq)] +/// Fee calculation error. +pub enum FeeError { + /// Transaction misses input. + MissingInput(usize), + /// Inputs sum overflow. + InputsOverflow, + /// Outputs sum overflow. + OutputsOverflow, + /// Negative fee. + NegativeFee, +} + +impl From for TransactionError { + fn from(error: FeeError) -> TransactionError { + match error { + FeeError::MissingInput(idx) => TransactionError::Input(idx), + FeeError::InputsOverflow => TransactionError::InputValueOverflow, + FeeError::OutputsOverflow => TransactionError::OutputValueOverflow, + FeeError::NegativeFee => TransactionError::Overspend, + } + } +} diff --git a/verification/src/fee.rs b/verification/src/fee.rs new file mode 100644 index 00000000..ac31c0d8 --- /dev/null +++ b/verification/src/fee.rs @@ -0,0 +1,110 @@ +use chain::Transaction; +use storage::TransactionOutputProvider; +use FeeError; + +/// Compute miner fee for given transaction. +/// +/// Returns None if overflow/underflow happens during computation. Missed prevout +/// is treated as 0-value. +pub fn checked_transaction_fee(store: &TransactionOutputProvider, tx_idx: usize, tx: &Transaction) -> Result { + // (1) Total sum of all transparent + shielded inputs + let mut incoming: u64 = 0; + for (input_idx, input) in tx.inputs.iter().enumerate() { + let prevout = match store.transaction_output(&input.previous_output, tx_idx) { + Some(prevout) => prevout, + None => return Err(FeeError::MissingInput(input_idx)), + }; + incoming = match incoming.checked_add(prevout.value) { + Some(incoming) => incoming, + None => return Err(FeeError::InputsOverflow), + }; + } + + if let Some(ref join_split) = tx.join_split { + for js_desc in &join_split.descriptions { + incoming = match incoming.checked_add(js_desc.value_pub_new) { + Some(incoming) => incoming, + None => return Err(FeeError::InputsOverflow), + }; + } + } + + if let Some(ref sapling) = tx.sapling { + if sapling.balancing_value > 0 { + let balancing_value = sapling.balancing_value as u64; + + incoming = match incoming.checked_add(balancing_value) { + Some(incoming) => incoming, + None => return Err(FeeError::InputsOverflow), + }; + } + } + + // (2) Total sum of all outputs + let mut spends = tx.total_spends(); + + if let Some(ref join_split) = tx.join_split { + for js_desc in &join_split.descriptions { + spends = match spends.checked_add(js_desc.value_pub_old) { + Some(spends) => spends, + None => return Err(FeeError::OutputsOverflow), + }; + } + } + + if let Some(ref sapling) = tx.sapling { + if sapling.balancing_value < 0 { + let balancing_value = match sapling.balancing_value.checked_neg() { + Some(balancing_value) => balancing_value as u64, + None => return Err(FeeError::OutputsOverflow), + }; + + spends = match spends.checked_add(balancing_value) { + Some(spends) => spends, + None => return Err(FeeError::OutputsOverflow), + }; + } + } + + // (3) Fee is the difference between (1) and (2) + match incoming.checked_sub(spends) { + Some(fee) => Ok(fee), + None => Err(FeeError::NegativeFee), + } +} + +#[cfg(test)] +mod tests { + extern crate test_data; + + use std::sync::Arc; + use storage::AsSubstore; + use db::BlockChainDatabase; + use super::*; + + #[test] + fn test_transaction_fee() { + let b0 = test_data::block_builder().header().nonce(1.into()).build() + .transaction() + .output().value(1_000_000).build() + .output().value(2_000_000).build() + .build() + .build(); + let tx0 = b0.transactions[0].clone(); + let tx0_hash = tx0.hash(); + let b1 = test_data::block_builder().header().parent(b0.hash().clone()).nonce(2.into()).build() + .transaction() + .input().hash(tx0_hash.clone()).index(0).build() + .input().hash(tx0_hash).index(1).build() + .output().value(2_500_000).build() + .build() + .build(); + let tx2 = b1.transactions[0].clone(); + + let db = Arc::new(BlockChainDatabase::init_test_chain(vec![b0.into(), b1.into()])); + let store = db.as_transaction_output_provider(); + + assert_eq!(checked_transaction_fee(store, ::std::usize::MAX, &tx0), Err(FeeError::NegativeFee)); + assert_eq!(checked_transaction_fee(store, ::std::usize::MAX, &tx2), Ok(500_000)); + } +} diff --git a/verification/src/lib.rs b/verification/src/lib.rs index 57a17ae3..b3e42017 100644 --- a/verification/src/lib.rs +++ b/verification/src/lib.rs @@ -85,6 +85,7 @@ mod canon; mod deployments; mod equihash; mod error; +mod fee; mod sapling; mod sigops; mod sprout; @@ -122,7 +123,8 @@ pub use verify_header::HeaderVerifier; pub use verify_transaction::{TransactionVerifier, MemoryPoolTransactionVerifier}; pub use chain_verifier::BackwardsCompatibleChainVerifier; -pub use error::{Error, TransactionError}; +pub use error::{Error, TransactionError, FeeError}; +pub use fee::checked_transaction_fee; pub use sigops::transaction_sigops; pub use timestamp::{median_timestamp, median_timestamp_inclusive}; pub use work::{work_required, is_valid_proof_of_work, is_valid_proof_of_work_hash};