feat(mempool): add ZIP-317 rules to mempool (#6556)

* add ZIP-317 rules to mempool

* fix some docs

* rustfmt

* fix import

* typo

* fix tests

* fix tests 2

* fix tests 3

* fix tests 4

* fix tests 5

* move constant

* fix constant for feature

* document/quote zip rules

* add Minimum Fee Rate rule

* change(mempool): Refactor the ZIP-317 minimum fee rate calculation to use usize (#6585)

* Refactor the minimum fee rate calculation to use usize

* Check for overflow if constants change

* remove 1 rule check, fix docs

---------

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2023-05-01 21:13:33 -03:00 committed by GitHub
parent f424b9cbf4
commit 8075d616fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 482 additions and 71 deletions

View File

@ -34,7 +34,9 @@ pub use serialize::{
MIN_TRANSPARENT_TX_V5_SIZE, MIN_TRANSPARENT_TX_V5_SIZE,
}; };
pub use sighash::{HashType, SigHash}; pub use sighash::{HashType, SigHash};
pub use unmined::{UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD}; pub use unmined::{
zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD,
};
use crate::{ use crate::{
amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative}, amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative},

View File

@ -36,7 +36,7 @@ use proptest_derive::Arbitrary;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::block::MAX_BLOCK_BYTES; use crate::block::MAX_BLOCK_BYTES;
mod zip317; pub mod zip317;
/// The minimum cost value for a transaction in the mempool. /// The minimum cost value for a transaction in the mempool.
/// ///
@ -353,17 +353,19 @@ impl VerifiedUnminedTx {
transaction: UnminedTx, transaction: UnminedTx,
miner_fee: Amount<NonNegative>, miner_fee: Amount<NonNegative>,
legacy_sigop_count: u64, legacy_sigop_count: u64,
) -> Self { ) -> Result<Self, zip317::Error> {
let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee); let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee);
let unpaid_actions = zip317::unpaid_actions(&transaction, miner_fee); let unpaid_actions = zip317::unpaid_actions(&transaction, miner_fee);
Self { zip317::mempool_checks(unpaid_actions, miner_fee, transaction.size)?;
Ok(Self {
transaction, transaction,
miner_fee, miner_fee,
legacy_sigop_count, legacy_sigop_count,
fee_weight_ratio, fee_weight_ratio,
unpaid_actions, unpaid_actions,
} })
} }
/// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee. /// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee.

View File

@ -5,6 +5,7 @@
use std::cmp::max; use std::cmp::max;
use num_integer::div_ceil; use num_integer::div_ceil;
use thiserror::Error;
use crate::{ use crate::{
amount::{Amount, NonNegative}, amount::{Amount, NonNegative},
@ -13,6 +14,9 @@ use crate::{
transaction::{Transaction, UnminedTx}, transaction::{Transaction, UnminedTx},
}; };
#[cfg(test)]
mod tests;
/// The marginal fee for the ZIP-317 fee calculation, in zatoshis per logical action. /// The marginal fee for the ZIP-317 fee calculation, in zatoshis per logical action.
// //
// TODO: allow Amount<NonNegative> in constants // TODO: allow Amount<NonNegative> in constants
@ -37,6 +41,27 @@ const BLOCK_PRODUCTION_WEIGHT_RATIO_CAP: f32 = 4.0;
/// This avoids special handling for transactions with zero weight. /// This avoids special handling for transactions with zero weight.
const MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE: i64 = 1; const MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE: i64 = 1;
/// The ZIP-317 recommended limit on the number of unpaid actions per block.
/// `block_unpaid_action_limit` in ZIP-317.
pub const BLOCK_PRODUCTION_UNPAID_ACTION_LIMIT: u32 = 50;
/// The minimum fee per kilobyte for Zebra mempool transactions.
/// Also used as the minimum fee for a mempool transaction.
///
/// Based on `DEFAULT_MIN_RELAY_TX_FEE` in `zcashd`:
/// <https://github.com/zcash/zcash/blob/f512291ff20098291442e83713de89bcddc07546/src/main.h#L71-L72>
///
/// This is a `usize` to simplify transaction size-based calculation code.
pub const MIN_MEMPOOL_TX_FEE_RATE: usize = 100;
/// The fee cap for [`MIN_MEMPOOL_TX_FEE_RATE`] minimum required mempool fees.
///
/// Based on `LEGACY_DEFAULT_FEE` in `zcashd`:
/// <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.h#L35-L36>
///
/// This is a `usize` to simplify transaction size-based calculation code.
pub const MEMPOOL_TX_FEE_REQUIREMENT_CAP: usize = 1000;
/// Returns the conventional fee for `transaction`, as defined by [ZIP-317]. /// Returns the conventional fee for `transaction`, as defined by [ZIP-317].
/// ///
/// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation /// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation
@ -139,3 +164,72 @@ fn conventional_actions(transaction: &Transaction) -> u32 {
max(GRACE_ACTIONS, logical_actions) max(GRACE_ACTIONS, logical_actions)
} }
/// Make ZIP-317 checks before inserting a transaction into the mempool.
pub fn mempool_checks(
unpaid_actions: u32,
miner_fee: Amount<NonNegative>,
transaction_size: usize,
) -> Result<(), Error> {
// # Standard Rule
//
// > If a transaction has more than `block_unpaid_action_limit` "unpaid actions" as defined by the
// > Recommended algorithm for block template construction, it will never be mined by that algorithm.
// > Nodes MAY drop these transactions.
//
// <https://zips.z.cash/zip-0317#transaction-relaying>
if unpaid_actions > BLOCK_PRODUCTION_UNPAID_ACTION_LIMIT {
return Err(Error::UnpaidActions);
}
// # Standard Rule
//
// > Nodes that normally relay transactions are expected to do so for transactions that pay at least the
// > conventional fee as specified in this ZIP.
//
// <https://zips.z.cash/zip-0317#transaction-relaying>
//
// In Zebra, we use a similar minimum fee rate to `zcashd` v5.5.0 and later.
// Transactions must pay a fee of at least 100 zatoshis per 1000 bytes of serialized size,
// with a maximum fee of 1000 zatoshis.
//
// <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.cpp#L24-L37>
//
// In zcashd this is `DEFAULT_MIN_RELAY_TX_FEE` and `LEGACY_DEFAULT_FEE`:
// <https://github.com/zcash/zcash/blob/f512291ff20098291442e83713de89bcddc07546/src/main.h#L71-L72>
// <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.h#L35-L36>
const KILOBYTE: usize = 1000;
// This calculation can't overflow, because transactions are limited to 2 MB,
// and usize is at least 4 GB.
assert!(
MIN_MEMPOOL_TX_FEE_RATE
< usize::MAX / usize::try_from(MAX_BLOCK_BYTES).expect("constant fits in usize"),
"the fee rate multiplication must never overflow",
);
let min_fee = (MIN_MEMPOOL_TX_FEE_RATE * transaction_size / KILOBYTE)
.clamp(MIN_MEMPOOL_TX_FEE_RATE, MEMPOOL_TX_FEE_REQUIREMENT_CAP);
let min_fee: u64 = min_fee
.try_into()
.expect("clamped value always fits in u64");
let min_fee: Amount<NonNegative> = min_fee.try_into().expect("clamped value is positive");
if miner_fee < min_fee {
return Err(Error::FeeBelowMinimumRate);
}
Ok(())
}
/// Errors related to ZIP-317.
#[derive(Error, Copy, Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Error {
#[error("Unpaid actions is higher than the limit")]
UnpaidActions,
#[error("Transaction fee is below the minimum fee rate")]
FeeBelowMinimumRate,
}

View File

@ -0,0 +1,18 @@
//! ZIP-317 tests.
use super::{mempool_checks, Amount, Error};
#[test]
fn zip317_unpaid_actions_err() {
let check = mempool_checks(51, Amount::try_from(1).unwrap(), 1);
assert!(check.is_err());
assert_eq!(check.err(), Some(Error::UnpaidActions));
}
#[test]
fn zip317_minimum_rate_fee_err() {
let check = mempool_checks(50, Amount::try_from(1).unwrap(), 1000);
assert!(check.is_err());
assert_eq!(check.err(), Some(Error::FeeBelowMinimumRate));
}

View File

@ -221,6 +221,10 @@ pub enum TransactionError {
outpoint: transparent::OutPoint, outpoint: transparent::OutPoint,
min_spend_height: block::Height, min_spend_height: block::Height,
}, },
#[error("failed to verify ZIP-317 transaction rules, transaction was not inserted to mempool")]
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
Zip317(#[from] zebra_chain::transaction::zip317::Error),
} }
impl From<ValidateContextError> for TransactionError { impl From<ValidateContextError> for TransactionError {

View File

@ -466,14 +466,15 @@ where
miner_fee, miner_fee,
legacy_sigop_count, legacy_sigop_count,
}, },
Request::Mempool { transaction, .. } => Response::Mempool { Request::Mempool { transaction, .. } => {
transaction: VerifiedUnminedTx::new( let transaction = VerifiedUnminedTx::new(
transaction, transaction,
miner_fee.expect( miner_fee.expect(
"unexpected mempool coinbase transaction: should have already rejected", "unexpected mempool coinbase transaction: should have already rejected",
), ),
legacy_sigop_count, legacy_sigop_count,
), )?;
Response::Mempool { transaction }
}, },
}; };

View File

@ -23,7 +23,7 @@ use zebra_chain::{
fake_v5_transactions_for_network, insert_fake_orchard_shielded_data, test_transactions, fake_v5_transactions_for_network, insert_fake_orchard_shielded_data, test_transactions,
transactions_from_blocks, transactions_from_blocks,
}, },
Hash, HashType, JoinSplitData, LockTime, Transaction, zip317, Hash, HashType, JoinSplitData, LockTime, Transaction,
}, },
transparent::{self, CoinbaseData}, transparent::{self, CoinbaseData},
}; };
@ -240,7 +240,12 @@ async fn mempool_request_with_present_input_is_accepted() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let tx = Transaction::V4 { let tx = Transaction::V4 {
@ -302,7 +307,12 @@ async fn mempool_request_with_invalid_lock_time_is_rejected() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let tx = Transaction::V4 { let tx = Transaction::V4 {
@ -376,7 +386,12 @@ async fn mempool_request_with_unlocked_lock_time_is_accepted() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let tx = Transaction::V4 { let tx = Transaction::V4 {
@ -438,7 +453,12 @@ async fn mempool_request_with_lock_time_max_sequence_number_is_accepted() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (mut input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (mut input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Ignore the lock time. // Ignore the lock time.
input.set_sequence(u32::MAX); input.set_sequence(u32::MAX);
@ -503,7 +523,12 @@ async fn mempool_request_with_past_lock_time_is_accepted() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let tx = Transaction::V4 { let tx = Transaction::V4 {
@ -577,7 +602,12 @@ async fn mempool_request_with_immature_spent_is_rejected() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small"); let fund_height = (height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let tx = Transaction::V4 { let tx = Transaction::V4 {
@ -836,7 +866,12 @@ async fn v4_transaction_with_transparent_transfer_is_accepted() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should succeed // Create a fake transparent transfer that should succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V4 transaction // Create a V4 transaction
let transaction = Transaction::V4 { let transaction = Transaction::V4 {
@ -881,7 +916,12 @@ async fn v4_transaction_with_last_valid_expiry_height() {
.activation_height(Network::Mainnet) .activation_height(Network::Mainnet)
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a non-coinbase V4 tx with the last valid expiry height. // Create a non-coinbase V4 tx with the last valid expiry height.
let transaction = Transaction::V4 { let transaction = Transaction::V4 {
@ -965,7 +1005,12 @@ async fn v4_transaction_with_too_low_expiry_height() {
.expect("Canopy activation height is specified"); .expect("Canopy activation height is specified");
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// This expiry height is too low so that the tx should seem expired to the verifier. // This expiry height is too low so that the tx should seem expired to the verifier.
let expiry_height = (block_height - 1).expect("original block height is too small"); let expiry_height = (block_height - 1).expect("original block height is too small");
@ -1010,7 +1055,12 @@ async fn v4_transaction_with_exceeding_expiry_height() {
let block_height = block::Height::MAX; let block_height = block::Height::MAX;
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// This expiry height exceeds the maximum defined by the specification. // This expiry height exceeds the maximum defined by the specification.
let expiry_height = block::Height(500_000_000); let expiry_height = block::Height(500_000_000);
@ -1163,7 +1213,12 @@ async fn v4_transaction_with_transparent_transfer_is_rejected_by_the_script() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should not succeed // Create a fake transparent transfer that should not succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
false,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V4 transaction // Create a V4 transaction
let transaction = Transaction::V4 { let transaction = Transaction::V4 {
@ -1213,7 +1268,12 @@ async fn v4_transaction_with_conflicting_transparent_spend_is_rejected() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should succeed // Create a fake transparent transfer that should succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V4 transaction // Create a V4 transaction
let transaction = Transaction::V4 { let transaction = Transaction::V4 {
@ -1401,7 +1461,12 @@ async fn v5_transaction_with_transparent_transfer_is_accepted() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should succeed // Create a fake transparent transfer that should succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V5 transaction // Create a V5 transaction
let transaction = Transaction::V5 { let transaction = Transaction::V5 {
@ -1447,7 +1512,12 @@ async fn v5_transaction_with_last_valid_expiry_height() {
.activation_height(Network::Testnet) .activation_height(Network::Testnet)
.expect("Nu5 activation height for testnet is specified"); .expect("Nu5 activation height for testnet is specified");
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a non-coinbase V5 tx with the last valid expiry height. // Create a non-coinbase V5 tx with the last valid expiry height.
let transaction = Transaction::V5 { let transaction = Transaction::V5 {
@ -1602,7 +1672,12 @@ async fn v5_transaction_with_too_low_expiry_height() {
.activation_height(Network::Testnet) .activation_height(Network::Testnet)
.expect("Nu5 activation height for testnet is specified"); .expect("Nu5 activation height for testnet is specified");
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// This expiry height is too low so that the tx should seem expired to the verifier. // This expiry height is too low so that the tx should seem expired to the verifier.
let expiry_height = (block_height - 1).expect("original block height is too small"); let expiry_height = (block_height - 1).expect("original block height is too small");
@ -1648,7 +1723,12 @@ async fn v5_transaction_with_exceeding_expiry_height() {
let block_height = block::Height::MAX; let block_height = block::Height::MAX;
let fund_height = (block_height - 1).expect("fake source fund block height is too small"); let fund_height = (block_height - 1).expect("fake source fund block height is too small");
let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// This expiry height exceeds the maximum defined by the specification. // This expiry height exceeds the maximum defined by the specification.
let expiry_height = block::Height(500_000_000); let expiry_height = block::Height(500_000_000);
@ -1753,7 +1833,12 @@ async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should not succeed // Create a fake transparent transfer that should not succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, false, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
false,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V5 transaction // Create a V5 transaction
let transaction = Transaction::V5 { let transaction = Transaction::V5 {
@ -1805,7 +1890,12 @@ async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() {
(transaction_block_height - 1).expect("fake source fund block height is too small"); (transaction_block_height - 1).expect("fake source fund block height is too small");
// Create a fake transparent transfer that should succeed // Create a fake transparent transfer that should succeed
let (input, output, known_utxos) = mock_transparent_transfer(fake_source_fund_height, true, 0); let (input, output, known_utxos) = mock_transparent_transfer(
fake_source_fund_height,
true,
0,
Amount::try_from(1).expect("invalid value"),
);
// Create a V4 transaction // Create a V4 transaction
let transaction = Transaction::V5 { let transaction = Transaction::V5 {
@ -2263,6 +2353,7 @@ fn mock_transparent_transfer(
previous_utxo_height: block::Height, previous_utxo_height: block::Height,
script_should_succeed: bool, script_should_succeed: bool,
outpoint_index: u32, outpoint_index: u32,
previous_output_value: Amount<NonNegative>,
) -> ( ) -> (
transparent::Input, transparent::Input,
transparent::Output, transparent::Output,
@ -2286,7 +2377,7 @@ fn mock_transparent_transfer(
}; };
let previous_output = transparent::Output { let previous_output = transparent::Output {
value: Amount::try_from(1).expect("1 is an invalid amount"), value: previous_output_value,
lock_script, lock_script,
}; };
@ -2726,3 +2817,145 @@ fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() {
); );
} }
} }
#[tokio::test]
async fn mempool_zip317_error() {
let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests();
let verifier = Verifier::new(Network::Mainnet, state.clone());
let height = NetworkUpgrade::Nu5
.activation_height(Network::Mainnet)
.expect("Nu5 activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small");
// Will produce a small enough miner fee to fail the check.
let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10).expect("invalid value"),
);
// Create a non-coinbase V5 tx.
let tx = Transaction::V5 {
inputs: vec![input],
outputs: vec![output],
lock_time: LockTime::unlocked(),
network_upgrade: NetworkUpgrade::Nu5,
expiry_height: height,
sapling_shielded_data: None,
orchard_shielded_data: None,
};
let input_outpoint = match tx.inputs()[0] {
transparent::Input::PrevOut { outpoint, .. } => outpoint,
transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"),
};
tokio::spawn(async move {
state
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
.await
.expect("verifier should call mock state service with correct request")
.respond(zebra_state::Response::UnspentBestChainUtxo(
known_utxos
.get(&input_outpoint)
.map(|utxo| utxo.utxo.clone()),
));
state
.expect_request_that(|req| {
matches!(
req,
zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_)
)
})
.await
.expect("verifier should call mock state service with correct request")
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
});
let verifier_response = verifier
.oneshot(Request::Mempool {
transaction: tx.into(),
height,
})
.await;
// Mempool refuses to add this transaction into storage.
assert!(verifier_response.is_err());
assert_eq!(
verifier_response.err(),
Some(TransactionError::Zip317(zip317::Error::FeeBelowMinimumRate))
);
}
#[tokio::test]
async fn mempool_zip317_ok() {
let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests();
let verifier = Verifier::new(Network::Mainnet, state.clone());
let height = NetworkUpgrade::Nu5
.activation_height(Network::Mainnet)
.expect("Nu5 activation height is specified");
let fund_height = (height - 1).expect("fake source fund block height is too small");
// Will produce a big enough miner fee to pass the check.
let (input, output, known_utxos) = mock_transparent_transfer(
fund_height,
true,
0,
Amount::try_from(10001).expect("invalid value"),
);
// Create a non-coinbase V5 tx.
let tx = Transaction::V5 {
inputs: vec![input],
outputs: vec![output],
lock_time: LockTime::unlocked(),
network_upgrade: NetworkUpgrade::Nu5,
expiry_height: height,
sapling_shielded_data: None,
orchard_shielded_data: None,
};
let input_outpoint = match tx.inputs()[0] {
transparent::Input::PrevOut { outpoint, .. } => outpoint,
transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"),
};
tokio::spawn(async move {
state
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
.await
.expect("verifier should call mock state service with correct request")
.respond(zebra_state::Response::UnspentBestChainUtxo(
known_utxos
.get(&input_outpoint)
.map(|utxo| utxo.utxo.clone()),
));
state
.expect_request_that(|req| {
matches!(
req,
zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_)
)
})
.await
.expect("verifier should call mock state service with correct request")
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
});
let verifier_response = verifier
.oneshot(Request::Mempool {
transaction: tx.into(),
height,
})
.await;
assert!(
verifier_response.is_ok(),
"expected successful verification, got: {verifier_response:?}"
);
}

View File

@ -7,6 +7,7 @@ use proptest::{collection::vec, prelude::*};
use tower::ServiceExt; use tower::ServiceExt;
use zebra_chain::{ use zebra_chain::{
amount::Amount,
block, block,
parameters::{Network, NetworkUpgrade}, parameters::{Network, NetworkUpgrade},
serialization::arbitrary::{datetime_full, datetime_u32}, serialization::arbitrary::{datetime_full, datetime_u32},
@ -387,8 +388,12 @@ fn mock_transparent_transfers(
.try_into() .try_into()
.expect("too many mock transparent transfers requested"); .expect("too many mock transparent transfers requested");
let (input, output, new_utxos) = let (input, output, new_utxos) = mock_transparent_transfer(
mock_transparent_transfer(fake_source_fund_height, true, outpoint_index); fake_source_fund_height,
true,
outpoint_index,
Amount::try_from(1).expect("invalid value"),
);
inputs.push(input); inputs.push(input);
outputs.push(output); outputs.push(output);

View File

@ -15,7 +15,7 @@ use zebra_chain::{
amount::NegativeOrZero, amount::NegativeOrZero,
block::{Height, MAX_BLOCK_BYTES}, block::{Height, MAX_BLOCK_BYTES},
parameters::Network, parameters::Network,
transaction::VerifiedUnminedTx, transaction::{zip317::BLOCK_PRODUCTION_UNPAID_ACTION_LIMIT, VerifiedUnminedTx},
transparent, transparent,
}; };
use zebra_consensus::MAX_BLOCK_SIGOPS; use zebra_consensus::MAX_BLOCK_SIGOPS;
@ -24,10 +24,6 @@ use crate::methods::get_block_template_rpcs::{
get_block_template::generate_coinbase_transaction, types::transaction::TransactionTemplate, get_block_template::generate_coinbase_transaction, types::transaction::TransactionTemplate,
}; };
/// The ZIP-317 recommended limit on the number of unpaid actions per block.
/// `block_unpaid_action_limit` in ZIP-317.
pub const BLOCK_PRODUCTION_UNPAID_ACTION_LIMIT: u32 = 50;
/// Selects mempool transactions for block production according to [ZIP-317], /// Selects mempool transactions for block production according to [ZIP-317],
/// using a fake coinbase transaction and the mempool. /// using a fake coinbase transaction and the mempool.
/// ///

View File

@ -164,11 +164,14 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}); });
let (push_response, _) = futures::join!(request, verification); let (push_response, _) = futures::join!(request, verification);
@ -266,11 +269,14 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}); });
let (advertise_response, _, _) = futures::join!(request, peer_set_responder, verification); let (advertise_response, _, _) = futures::join!(request, peer_set_responder, verification);
@ -365,11 +371,14 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}); });
let (push_response, _) = futures::join!(request, verification); let (push_response, _) = futures::join!(request, verification);
@ -499,11 +508,14 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}); });
let (push_response, _) = futures::join!(request, verification); let (push_response, _) = futures::join!(request, verification);

View File

@ -37,5 +37,12 @@ pub fn unmined_transactions_in_blocks(
selected_blocks selected_blocks
.flat_map(|block| block.transactions) .flat_map(|block| block.transactions)
.map(UnminedTx::from) .map(UnminedTx::from)
.map(|transaction| VerifiedUnminedTx::new(transaction, Amount::zero(), 0)) .map(|transaction| {
VerifiedUnminedTx::new(
transaction,
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass")
})
} }

View File

@ -475,8 +475,20 @@ impl SpendConflictTestInput {
}; };
( (
VerifiedUnminedTx::new(first.0.into(), Amount::zero(), 0), VerifiedUnminedTx::new(
VerifiedUnminedTx::new(second.0.into(), Amount::zero(), 0), first.0.into(),
// make sure miner fee is big enough for all cases
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass"),
VerifiedUnminedTx::new(
second.0.into(),
// make sure miner fee is big enough for all cases
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass"),
) )
} }
@ -493,8 +505,20 @@ impl SpendConflictTestInput {
Self::remove_orchard_conflicts(&mut first, &mut second); Self::remove_orchard_conflicts(&mut first, &mut second);
( (
VerifiedUnminedTx::new(first.0.into(), Amount::zero(), 0), VerifiedUnminedTx::new(
VerifiedUnminedTx::new(second.0.into(), Amount::zero(), 0), first.0.into(),
// make sure miner fee is big enough for all cases
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass"),
VerifiedUnminedTx::new(
second.0.into(),
// make sure miner fee is big enough for all cases
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass"),
) )
} }

View File

@ -271,7 +271,14 @@ fn mempool_expired_basic_for_network(network: Network) -> Result<()> {
let tx_id = tx.unmined_id(); let tx_id = tx.unmined_id();
// Insert the transaction into the mempool, with a fake zero miner fee and sigops // Insert the transaction into the mempool, with a fake zero miner fee and sigops
storage.insert(VerifiedUnminedTx::new(tx.into(), Amount::zero(), 0))?; storage.insert(
VerifiedUnminedTx::new(
tx.into(),
Amount::try_from(1_000_000).expect("invalid value"),
0,
)
.expect("verification should pass"),
)?;
assert_eq!(storage.transaction_count(), 1); assert_eq!(storage.transaction_count(), 1);

View File

@ -805,11 +805,14 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}) })
.await; .await;
@ -862,11 +865,14 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> {
.expect("unexpected non-mempool request"); .expect("unexpected non-mempool request");
// Set a dummy fee and sigops. // Set a dummy fee and sigops.
responder.respond(transaction::Response::from(VerifiedUnminedTx::new( responder.respond(transaction::Response::from(
VerifiedUnminedTx::new(
transaction, transaction,
Amount::zero(), Amount::try_from(1_000_000).expect("invalid value"),
0, 0,
))); )
.expect("verification should pass"),
));
}) })
.await; .await;