unify fee calculation

This commit is contained in:
Svyatoslav Nikolsky 2019-04-15 19:03:55 +03:00
parent 3bd0c73299
commit 7cc25b7e6c
6 changed files with 157 additions and 135 deletions

View File

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

View File

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

View File

@ -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::<u64>();
let available_join_split = self.transaction.raw.join_split.iter()
.flat_map(|js| &js.descriptions)
.map(|d| d.value_pub_new)
.sum::<u64>();
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(|_| ())
}
}

View File

@ -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<FeeError> 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,
}
}
}

110
verification/src/fee.rs Normal file
View File

@ -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<u64, FeeError> {
// (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));
}
}

View File

@ -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};