unify fee calculation
This commit is contained in:
parent
3bd0c73299
commit
7cc25b7e6c
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
Loading…
Reference in New Issue