Skip to main content

kaspa_mining/
manager.rs

1use crate::{
2    MempoolCountersSnapshot, MiningCounters, P2pTxCountSample,
3    block_template::{builder::BlockTemplateBuilder, errors::BuilderError},
4    cache::BlockTemplateCache,
5    errors::MiningManagerResult,
6    feerate::{FeeEstimateVerbose, FeerateEstimations, FeerateEstimatorArgs},
7    mempool::{
8        Mempool,
9        config::Config,
10        model::tx::{MempoolTransaction, TransactionPostValidation, TransactionPreValidation, TxRemovalReason},
11        populate_entries_and_try_validate::{
12            PopulateError, populate_mempool_transactions_in_parallel, validate_mempool_transaction,
13            validate_mempool_transactions_in_parallel,
14        },
15        tx::{Orphan, Priority, RbfPolicy},
16    },
17    model::{
18        owner_txs::{GroupedOwnerTransactions, ScriptPublicKeySet},
19        topological_sort::IntoIterTopologically,
20        tx_insert::TransactionInsertion,
21        tx_query::TransactionQuery,
22    },
23};
24use itertools::Itertools;
25use kaspa_consensus_core::{
26    api::{
27        ConsensusApi,
28        args::{TransactionValidationArgs, TransactionValidationBatchArgs},
29    },
30    block::{BlockTemplate, TemplateBuildMode, TemplateTransactionSelector},
31    coinbase::MinerData,
32    errors::{block::RuleError as BlockRuleError, tx::TxRuleError},
33    mass::{BlockLaneLimits, BlockMassLimits},
34    tx::{MutableTransaction, Transaction, TransactionId},
35};
36use kaspa_consensusmanager::{ConsensusProxy, spawn_blocking};
37use kaspa_core::{debug, error, info, time::Stopwatch, warn};
38use kaspa_mining_errors::{manager::MiningManagerError, mempool::RuleError};
39use parking_lot::RwLock;
40use std::sync::Arc;
41use tokio::sync::mpsc::UnboundedSender;
42
43pub struct MiningManager {
44    config: Arc<Config>,
45    block_template_cache: BlockTemplateCache,
46    mempool: RwLock<Mempool>,
47    counters: Arc<MiningCounters>,
48}
49
50impl MiningManager {
51    pub fn new(
52        target_time_per_block: u64,
53        relay_non_std_transactions: bool,
54        mempool_block_mass_limits: BlockMassLimits,
55        block_lane_limits: BlockLaneLimits,
56        cache_lifetime: Option<u64>,
57        counters: Arc<MiningCounters>,
58    ) -> Self {
59        let config =
60            Config::build_default(target_time_per_block, relay_non_std_transactions, mempool_block_mass_limits, block_lane_limits);
61        Self::with_config(config, cache_lifetime, counters)
62    }
63
64    pub fn new_with_extended_config(
65        target_time_per_block: u64,
66        relay_non_std_transactions: bool,
67        mempool_block_mass_limits: BlockMassLimits,
68        block_lane_limits: BlockLaneLimits,
69        ram_scale: f64,
70        cache_lifetime: Option<u64>,
71        counters: Arc<MiningCounters>,
72    ) -> Self {
73        let config =
74            Config::build_default(target_time_per_block, relay_non_std_transactions, mempool_block_mass_limits, block_lane_limits)
75                .apply_ram_scale(ram_scale);
76        Self::with_config(config, cache_lifetime, counters)
77    }
78
79    pub(crate) fn with_config(config: Config, cache_lifetime: Option<u64>, counters: Arc<MiningCounters>) -> Self {
80        let config = Arc::new(config);
81        let mempool = RwLock::new(Mempool::new(config.clone(), counters.clone()));
82        let block_template_cache = BlockTemplateCache::new(cache_lifetime);
83        Self { config, block_template_cache, mempool, counters }
84    }
85
86    pub fn get_block_template(&self, consensus: &dyn ConsensusApi, miner_data: &MinerData) -> MiningManagerResult<BlockTemplate> {
87        let virtual_state_approx_id = consensus.get_virtual_state_approx_id();
88        let mut cache_lock = self.block_template_cache.lock(virtual_state_approx_id);
89        let immutable_template = cache_lock.get_immutable_cached_template();
90
91        // We first try and use a cached template if not expired
92        if let Some(immutable_template) = immutable_template {
93            drop(cache_lock);
94            if immutable_template.miner_data == *miner_data {
95                return Ok(immutable_template.as_ref().clone());
96            }
97            // Miner data is new -- make the minimum changes required
98            // Note the call returns a modified clone of the cached block template
99            let block_template = BlockTemplateBuilder::modify_block_template(consensus, miner_data, &immutable_template)?;
100
101            // No point in updating cache since we have no reason to believe this coinbase will be used more
102            // than the previous one, and we want to maintain the original template caching time
103            return Ok(block_template);
104        }
105
106        // Rust rewrite:
107        // We avoid passing a mempool ref to blockTemplateBuilder by calling
108        // mempool.BlockCandidateTransactions and mempool.RemoveTransactions here.
109        // We remove recursion seen in blockTemplateBuilder.BuildBlockTemplate here.
110        debug!("Building a new block template...");
111        let _swo = Stopwatch::<22>::with_threshold("build_block_template full loop");
112        let mut attempts: u64 = 0;
113        loop {
114            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
115            {
116                attempts += 1;
117            }
118
119            let selector = self.build_selector();
120            let block_template_builder = BlockTemplateBuilder::new();
121            let build_mode = if attempts < self.config.maximum_build_block_template_attempts {
122                TemplateBuildMode::Standard
123            } else {
124                TemplateBuildMode::Infallible
125            };
126            match block_template_builder.build_block_template(consensus, miner_data, selector, build_mode) {
127                Ok(block_template) => {
128                    let block_template = cache_lock.set_immutable_cached_template(block_template);
129                    match attempts {
130                        1 => {
131                            debug!(
132                                "Built a new block template with {} transactions in {:#?}",
133                                block_template.block.transactions.len(),
134                                _swo.elapsed()
135                            );
136                        }
137                        2 => {
138                            debug!(
139                                "Built a new block template with {} transactions at second attempt in {:#?}",
140                                block_template.block.transactions.len(),
141                                _swo.elapsed()
142                            );
143                        }
144                        n => {
145                            debug!(
146                                "Built a new block template with {} transactions in {} attempts totaling {:#?}",
147                                block_template.block.transactions.len(),
148                                n,
149                                _swo.elapsed()
150                            );
151                        }
152                    }
153                    return Ok(block_template.as_ref().clone());
154                }
155                Err(BuilderError::ConsensusError(BlockRuleError::InvalidTransactionsInNewBlock(invalid_transactions))) => {
156                    let mut missing_outpoint: usize = 0;
157                    let mut invalid: usize = 0;
158
159                    let mut mempool_write = self.mempool.write();
160                    invalid_transactions.iter().for_each(|(x, err)| {
161                        // On missing outpoints, the most likely is that the tx was already in a block accepted by
162                        // the consensus but not yet processed by handle_new_block_transactions(). Another possibility
163                        // is a double spend. In both cases, we simply remove the transaction but keep its redeemers.
164                        // Those will either be valid in a next block template or invalidated if it's a double spend.
165                        //
166                        // If the redeemers of a transaction accepted in consensus but not yet handled in mempool were
167                        // removed, it would lead to having subsequently submitted children transactions of the removed
168                        // redeemers being unexpectedly either orphaned or rejected in case orphans are disallowed.
169                        //
170                        // For all other errors, we do remove the redeemers.
171
172                        let removal_result = if *err == TxRuleError::MissingTxOutpoints {
173                            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
174                            {
175                                missing_outpoint += 1;
176                            }
177                            mempool_write.remove_transaction(x, false, TxRemovalReason::Muted, "")
178                        } else {
179                            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
180                            {
181                                invalid += 1;
182                            }
183                            warn!("Remove per BBT invalid transaction and descendants");
184                            mempool_write.remove_transaction(
185                                x,
186                                true,
187                                TxRemovalReason::InvalidInBlockTemplate,
188                                format!(" error: {}", err).as_str(),
189                            )
190                        };
191                        if let Err(err) = removal_result {
192                            // Original golang comment:
193                            // mempool.remove_transactions might return errors in situations that are perfectly fine in this context.
194                            // TODO: Once the mempool invariants are clear, this might return an error:
195                            // https://github.com/kaspanet/kaspad/issues/1553
196                            // NOTE: unlike golang, here we continue removing also if an error was found
197                            error!("Error from mempool.remove_transactions: {:?}", err);
198                        }
199                    });
200                    drop(mempool_write);
201
202                    debug!(
203                        "Building a new block template failed for {} txs missing outpoint and {} invalid txs",
204                        missing_outpoint, invalid
205                    );
206                }
207                Err(err) => {
208                    warn!("Building a new block template failed: {}", err);
209                    return Err(err)?;
210                }
211            }
212        }
213    }
214
215    /// Dynamically builds a transaction selector based on the specific state of the ready transactions frontier
216    pub(crate) fn build_selector(&self) -> Box<dyn TemplateTransactionSelector> {
217        self.mempool.read().build_selector()
218    }
219
220    /// Returns realtime feerate estimations based on internal mempool state
221    pub(crate) fn get_realtime_feerate_estimations(&self) -> FeerateEstimations {
222        let args = FeerateEstimatorArgs::new(self.config.network_blocks_per_second, self.config.mempool_mass_cofactors.reference);
223        let estimator = self.mempool.read().build_feerate_estimator(args);
224        estimator.calc_estimations(self.config.minimum_feerate())
225    }
226
227    /// Returns realtime feerate estimations based on internal mempool state with additional verbose data
228    pub(crate) fn get_realtime_feerate_estimations_verbose(
229        &self,
230        consensus: &dyn ConsensusApi,
231        prefix: kaspa_addresses::Prefix,
232    ) -> MiningManagerResult<FeeEstimateVerbose> {
233        let args = FeerateEstimatorArgs::new(self.config.network_blocks_per_second, self.config.mempool_mass_cofactors.reference);
234        let network_mass_per_second = args.network_mass_per_second();
235        let mempool_read = self.mempool.read();
236        let estimator = mempool_read.build_feerate_estimator(args);
237        let ready_transactions_count = mempool_read.ready_transaction_count();
238        let ready_transaction_total_mass = mempool_read.ready_transaction_total_mass();
239        drop(mempool_read);
240        let mut resp = FeeEstimateVerbose {
241            estimations: estimator.calc_estimations(self.config.minimum_feerate()),
242            network_mass_per_second,
243            mempool_ready_transactions_count: ready_transactions_count as u64,
244            mempool_ready_transactions_total_mass: ready_transaction_total_mass,
245
246            next_block_template_feerate_min: -1.0,
247            next_block_template_feerate_median: -1.0,
248            next_block_template_feerate_max: -1.0,
249        };
250        // calculate next_block_template_feerate_xxx
251        {
252            let script_public_key = kaspa_txscript::pay_to_address_script(&kaspa_addresses::Address::new(
253                prefix,
254                kaspa_addresses::Version::PubKey,
255                &[0u8; 32],
256            ));
257            let miner_data: MinerData = MinerData::new(script_public_key, vec![]);
258
259            let BlockTemplate { block: kaspa_consensus_core::block::MutableBlock { transactions, .. }, calculated_fees, .. } =
260                self.get_block_template(consensus, &miner_data)?;
261
262            let Some(Stats { max, median, min }) = feerate_stats(transactions, calculated_fees) else {
263                return Ok(resp);
264            };
265
266            resp.next_block_template_feerate_max = max;
267            resp.next_block_template_feerate_min = min;
268            resp.next_block_template_feerate_median = median;
269        }
270        Ok(resp)
271    }
272
273    /// Clears the block template cache, forcing the next call to get_block_template to build a new block template.
274    #[cfg(test)]
275    pub(crate) fn clear_block_template(&self) {
276        self.block_template_cache.clear();
277    }
278
279    #[cfg(test)]
280    pub(crate) fn block_template_builder(&self) -> BlockTemplateBuilder {
281        BlockTemplateBuilder::new()
282    }
283
284    /// validate_and_insert_transaction validates the given transaction, and
285    /// adds it to the set of known transactions that have not yet been
286    /// added to any block.
287    ///
288    /// The validation is constrained by a Replace by fee policy applied
289    /// to double spends in the mempool. For more information, see [`RbfPolicy`].
290    ///
291    /// On success, returns transactions that where unorphaned following the insertion
292    /// of the provided transaction.
293    ///
294    /// The returned transactions are references of objects owned by the mempool.
295    pub fn validate_and_insert_transaction(
296        &self,
297        consensus: &dyn ConsensusApi,
298        transaction: Transaction,
299        priority: Priority,
300        orphan: Orphan,
301        rbf_policy: RbfPolicy,
302    ) -> MiningManagerResult<TransactionInsertion> {
303        self.validate_and_insert_mutable_transaction(consensus, MutableTransaction::from_tx(transaction), priority, orphan, rbf_policy)
304    }
305
306    /// Exposed for tests only
307    ///
308    /// See `validate_and_insert_transaction`
309    pub(crate) fn validate_and_insert_mutable_transaction(
310        &self,
311        consensus: &dyn ConsensusApi,
312        transaction: MutableTransaction,
313        priority: Priority,
314        orphan: Orphan,
315        rbf_policy: RbfPolicy,
316    ) -> MiningManagerResult<TransactionInsertion> {
317        // read lock on mempool
318        let TransactionPreValidation { mut transaction, feerate_threshold } =
319            self.mempool.read().pre_validate_and_populate_transaction(consensus, transaction, rbf_policy)?;
320        let args = TransactionValidationArgs::new(feerate_threshold);
321        // no lock on mempool
322        let validation_result = validate_mempool_transaction(consensus, &mut transaction, &args);
323        // write lock on mempool
324        let mut mempool = self.mempool.write();
325        match mempool.post_validate_and_insert_transaction(consensus, validation_result, transaction, priority, orphan, rbf_policy)? {
326            TransactionPostValidation { removed, accepted: Some(accepted_transaction) } => {
327                let unorphaned_transactions = mempool.get_unorphaned_transactions_after_accepted_transaction(&accepted_transaction);
328                drop(mempool);
329
330                // The capacity used here may be exceeded since accepted unorphaned transaction may themselves unorphan other transactions.
331                #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(LENGTH)")]
332                let mut accepted_transactions = Vec::with_capacity(unorphaned_transactions.len() + 1);
333                // We include the original accepted transaction as well
334                accepted_transactions.push(accepted_transaction);
335                accepted_transactions.extend(self.validate_and_insert_unorphaned_transactions(consensus, unorphaned_transactions));
336                self.counters.increase_tx_counts(1, priority);
337
338                Ok(TransactionInsertion::new(removed, accepted_transactions))
339            }
340            TransactionPostValidation { removed, accepted: None } => Ok(TransactionInsertion::new(removed, vec![])),
341        }
342    }
343
344    fn validate_and_insert_unorphaned_transactions(
345        &self,
346        consensus: &dyn ConsensusApi,
347        mut incoming_transactions: Vec<MempoolTransaction>,
348    ) -> Vec<Arc<Transaction>> {
349        // The capacity used here may be exceeded (see next comment).
350        let mut accepted_transactions = Vec::with_capacity(incoming_transactions.len());
351        // The validation args map is immutably empty since unorphaned transactions do not require pre processing so there
352        // are no feerate thresholds to use. Instead, we rely on this being checked during post processing.
353        let args = TransactionValidationBatchArgs::new();
354        // We loop as long as incoming unorphaned transactions do unorphan other transactions when they
355        // get validated and inserted into the mempool.
356        while !incoming_transactions.is_empty() {
357            // Since the consensus validation requires a slice of MutableTransaction, we destructure the vector of
358            // MempoolTransaction into 2 distinct vectors holding respectively the needed MutableTransaction and Priority.
359            let (mut transactions, priorities): (Vec<MutableTransaction>, Vec<Priority>) =
360                incoming_transactions.into_iter().map(|x| (x.mtx, x.priority)).unzip();
361
362            // no lock on mempool
363            // We process the transactions by chunks of max block mass to prevent locking the virtual processor for too long.
364            let mut lower_bound: usize = 0;
365            let mut validation_results = Vec::with_capacity(transactions.len());
366            while let Some(upper_bound) = self.next_transaction_chunk_upper_bound(&transactions, lower_bound) {
367                assert!(lower_bound < upper_bound, "the chunk is never empty");
368                validation_results.extend(validate_mempool_transactions_in_parallel(
369                    consensus,
370                    &mut transactions[lower_bound..upper_bound],
371                    &args,
372                ));
373                lower_bound = upper_bound;
374            }
375            assert_eq!(transactions.len(), validation_results.len(), "every transaction should have a matching validation result");
376
377            // write lock on mempool
378            let mut mempool = self.mempool.write();
379            incoming_transactions = transactions
380                .into_iter()
381                .zip(priorities)
382                .zip(validation_results)
383                .flat_map(|((transaction, priority), validation_result)| {
384                    let orphan_id = transaction.id();
385                    let rbf_policy = Mempool::get_orphan_transaction_rbf_policy(priority);
386                    match mempool.post_validate_and_insert_transaction(
387                        consensus,
388                        validation_result,
389                        transaction,
390                        priority,
391                        Orphan::Forbidden,
392                        rbf_policy,
393                    ) {
394                        Ok(TransactionPostValidation { removed: _, accepted: Some(accepted_transaction) }) => {
395                            accepted_transactions.push(accepted_transaction.clone());
396                            self.counters.increase_tx_counts(1, priority);
397                            mempool.get_unorphaned_transactions_after_accepted_transaction(&accepted_transaction)
398                        }
399                        Ok(TransactionPostValidation { removed: _, accepted: None }) => vec![],
400                        Err(err) => {
401                            debug!("Failed to unorphan transaction {0} due to rule error: {1}", orphan_id, err);
402                            vec![]
403                        }
404                    }
405                })
406                .collect::<Vec<_>>();
407            drop(mempool);
408        }
409        accepted_transactions
410    }
411
412    /// Validates a batch of transactions, handling iteratively only the independent ones, and
413    /// adds those to the set of known transactions that have not yet been added to any block.
414    ///
415    /// The validation is constrained by a Replace by fee policy applied
416    /// to double spends in the mempool. For more information, see [`RbfPolicy`].
417    ///
418    /// Returns transactions that where unorphaned following the insertion of the provided
419    /// transactions. The returned transactions are references of objects owned by the mempool.
420    pub fn validate_and_insert_transaction_batch(
421        &self,
422        consensus: &dyn ConsensusApi,
423        transactions: Vec<Transaction>,
424        priority: Priority,
425        orphan: Orphan,
426        rbf_policy: RbfPolicy,
427    ) -> Vec<MiningManagerResult<Arc<Transaction>>> {
428        const TRANSACTION_CHUNK_SIZE: usize = 250;
429
430        // The capacity used here may be exceeded since accepted transactions may unorphan other transactions.
431        let mut insert_results: Vec<MiningManagerResult<Arc<Transaction>>> = Vec::with_capacity(transactions.len());
432        let mut unorphaned_transactions = vec![];
433        let _swo = Stopwatch::<80>::with_threshold("validate_and_insert_transaction_batch topological_sort op");
434        let sorted_transactions = transactions.into_iter().map(MutableTransaction::from_tx).topological_into_iter();
435        drop(_swo);
436
437        // read lock on mempool
438        // Here, we simply log and drop all erroneous transactions since the caller doesn't care about those anyway
439        let mut transactions = Vec::with_capacity(sorted_transactions.len());
440        let mut args = TransactionValidationBatchArgs::new();
441        for chunk in &sorted_transactions.chunks(TRANSACTION_CHUNK_SIZE) {
442            let mempool = self.mempool.read();
443            let txs = chunk.filter_map(|tx| {
444                let transaction_id = tx.id();
445                match mempool.pre_validate_and_populate_transaction(consensus, tx, rbf_policy) {
446                    Ok(TransactionPreValidation { transaction, feerate_threshold }) => {
447                        if let Some(threshold) = feerate_threshold {
448                            args.set_feerate_threshold(transaction.id(), threshold);
449                        }
450                        Some(transaction)
451                    }
452                    Err(RuleError::RejectAlreadyAccepted(transaction_id)) => {
453                        debug!("Ignoring already accepted transaction {}", transaction_id);
454                        None
455                    }
456                    Err(RuleError::RejectDuplicate(transaction_id)) => {
457                        debug!("Ignoring transaction already in the mempool {}", transaction_id);
458                        None
459                    }
460                    Err(RuleError::RejectDuplicateOrphan(transaction_id)) => {
461                        debug!("Ignoring transaction already in the orphan pool {}", transaction_id);
462                        None
463                    }
464                    Err(err) => {
465                        debug!("Failed to pre validate transaction {0} due to rule error: {1}", transaction_id, err);
466                        insert_results.push(Err(MiningManagerError::MempoolError(err)));
467                        None
468                    }
469                }
470            });
471            transactions.extend(txs);
472        }
473
474        // no lock on mempool
475        // We process the transactions by chunks of max block mass to prevent locking the virtual processor for too long.
476        let mut lower_bound: usize = 0;
477        let mut validation_results = Vec::with_capacity(transactions.len());
478        while let Some(upper_bound) = self.next_transaction_chunk_upper_bound(&transactions, lower_bound) {
479            assert!(lower_bound < upper_bound, "the chunk is never empty");
480            validation_results.extend(validate_mempool_transactions_in_parallel(
481                consensus,
482                &mut transactions[lower_bound..upper_bound],
483                &args,
484            ));
485            lower_bound = upper_bound;
486        }
487        assert_eq!(transactions.len(), validation_results.len(), "every transaction should have a matching validation result");
488
489        // write lock on mempool
490        // Here again, transactions failing post validation are logged and dropped
491        for chunk in &transactions.into_iter().zip(validation_results).chunks(TRANSACTION_CHUNK_SIZE) {
492            let mut mempool = self.mempool.write();
493            let txs = chunk.flat_map(|(transaction, validation_result)| {
494                let transaction_id = transaction.id();
495                match mempool.post_validate_and_insert_transaction(
496                    consensus,
497                    validation_result,
498                    transaction,
499                    priority,
500                    orphan,
501                    rbf_policy,
502                ) {
503                    Ok(TransactionPostValidation { removed: _, accepted: Some(accepted_transaction) }) => {
504                        insert_results.push(Ok(accepted_transaction.clone()));
505                        self.counters.increase_tx_counts(1, priority);
506                        mempool.get_unorphaned_transactions_after_accepted_transaction(&accepted_transaction)
507                    }
508                    Ok(TransactionPostValidation { removed: _, accepted: None }) | Err(RuleError::RejectDuplicate(_)) => {
509                        // Either orphaned or already existing in the mempool
510                        vec![]
511                    }
512                    Err(err) => {
513                        debug!("Failed to post validate transaction {0} due to rule error: {1}", transaction_id, err);
514                        insert_results.push(Err(MiningManagerError::MempoolError(err)));
515                        vec![]
516                    }
517                }
518            });
519            unorphaned_transactions.extend(txs);
520        }
521
522        insert_results
523            .extend(self.validate_and_insert_unorphaned_transactions(consensus, unorphaned_transactions).into_iter().map(Ok));
524        insert_results
525    }
526
527    fn next_transaction_chunk_upper_bound(&self, transactions: &[MutableTransaction], lower_bound: usize) -> Option<usize> {
528        if lower_bound >= transactions.len() {
529            return None;
530        }
531        let cofactors = self.config.mempool_mass_cofactors;
532        let mut mass: u64 = 0;
533        transactions[lower_bound..]
534            .iter()
535            .position(|tx| {
536                // If this overflows, we have some non standard transaction. This is fine, since it will be rejected by mempool validation.
537                mass = mass.saturating_add(tx.calculated_non_contextual_masses.unwrap().normalized_max(&cofactors));
538                mass >= cofactors.reference
539            })
540            // Make sure the upper bound is greater than the lower bound, allowing to handle a very unlikely,
541            // (if not impossible) case where the mass of a single transaction is greater than the maximum
542            // chunk mass.
543            .map(|relative_index| {
544                #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(INDEX)")]
545                {
546                    relative_index.max(1) + lower_bound
547                }
548            })
549            .or(Some(transactions.len()))
550    }
551
552    /// Try to return a mempool transaction by its id.
553    ///
554    /// Note: the transaction is an orphan if tx.is_fully_populated() returns false.
555    pub fn get_transaction(&self, transaction_id: &TransactionId, query: TransactionQuery) -> Option<MutableTransaction> {
556        self.mempool.read().get_transaction(transaction_id, query)
557    }
558
559    /// Returns whether the mempool holds this transaction in any form.
560    pub fn has_transaction(&self, transaction_id: &TransactionId, query: TransactionQuery) -> bool {
561        self.mempool.read().has_transaction(transaction_id, query)
562    }
563
564    pub fn get_all_transactions(&self, query: TransactionQuery) -> (Vec<MutableTransaction>, Vec<MutableTransaction>) {
565        const TRANSACTION_CHUNK_SIZE: usize = 1000;
566        // read lock on mempool by transaction chunks
567        let transactions = if query.include_transaction_pool() {
568            let transaction_ids = self.mempool.read().get_all_transaction_ids(TransactionQuery::TransactionsOnly).0;
569            let mut transactions = Vec::with_capacity(self.mempool.read().transaction_count(TransactionQuery::TransactionsOnly));
570            for chunks in transaction_ids.chunks(TRANSACTION_CHUNK_SIZE) {
571                let mempool = self.mempool.read();
572                transactions.extend(chunks.iter().filter_map(|x| mempool.get_transaction(x, TransactionQuery::TransactionsOnly)));
573            }
574            transactions
575        } else {
576            vec![]
577        };
578        // read lock on mempool
579        let orphans = if query.include_orphan_pool() {
580            self.mempool.read().get_all_transactions(TransactionQuery::OrphansOnly).1
581        } else {
582            vec![]
583        };
584        (transactions, orphans)
585    }
586
587    /// get_transactions_by_addresses returns the sending and receiving transactions for
588    /// a set of addresses.
589    ///
590    /// Note: a transaction is an orphan if tx.is_fully_populated() returns false.
591    pub fn get_transactions_by_addresses(
592        &self,
593        script_public_keys: &ScriptPublicKeySet,
594        query: TransactionQuery,
595    ) -> GroupedOwnerTransactions {
596        // TODO: break the monolithic lock
597        self.mempool.read().get_transactions_by_addresses(script_public_keys, query)
598    }
599
600    pub fn transaction_count(&self, query: TransactionQuery) -> usize {
601        self.mempool.read().transaction_count(query)
602    }
603
604    pub fn handle_new_block_transactions(
605        &self,
606        consensus: &dyn ConsensusApi,
607        block_daa_score: u64,
608        block_transactions: &[Transaction],
609    ) -> MiningManagerResult<Vec<Arc<Transaction>>> {
610        // TODO: should use tx acceptance data to verify that new block txs are actually accepted into virtual state.
611        // TODO: avoid returning a result from this function (and the underlying function). Any possible error is a
612        // problem of the internal implementation and unrelated to the caller
613
614        // write lock on mempool
615        let unorphaned_transactions = self.mempool.write().handle_new_block_transactions(block_daa_score, block_transactions)?;
616
617        // alternate no & write lock on mempool
618        let accepted_transactions = self.validate_and_insert_unorphaned_transactions(consensus, unorphaned_transactions);
619
620        Ok(accepted_transactions)
621    }
622
623    pub fn expire_low_priority_transactions(&self, consensus: &dyn ConsensusApi) {
624        // very fine-grained write locks on mempool
625        debug!("<> Expiring low priority transactions...");
626
627        // orphan pool
628        if let Err(err) = self.mempool.write().expire_orphan_low_priority_transactions(consensus) {
629            warn!("Failed to expire transactions from orphan pool: {}", err);
630        }
631
632        // accepted transaction cache
633        self.mempool.write().expire_accepted_transactions(consensus);
634
635        // mempool
636        let expired_low_priority_transactions = self.mempool.write().collect_expired_low_priority_transactions(consensus);
637        for chunk in &expired_low_priority_transactions.iter().chunks(24) {
638            let mut mempool = self.mempool.write();
639            chunk.into_iter().for_each(|tx| {
640                if let Err(err) = mempool.remove_transaction(tx, true, TxRemovalReason::Muted, "") {
641                    warn!("Failed to remove transaction {} from mempool: {}", tx, err);
642                }
643            });
644        }
645        match expired_low_priority_transactions.len() {
646            0 => {}
647            1 => debug!("Removed transaction ({}) {}", TxRemovalReason::Expired, expired_low_priority_transactions[0]),
648            n => debug!("Removed {} transactions ({}): {}...", n, TxRemovalReason::Expired, expired_low_priority_transactions[0]),
649        }
650    }
651
652    pub fn revalidate_high_priority_transactions(
653        &self,
654        consensus: &dyn ConsensusApi,
655        transaction_ids_sender: UnboundedSender<Vec<TransactionId>>,
656    ) {
657        const TRANSACTION_CHUNK_SIZE: usize = 1000;
658
659        // read lock on mempool
660        // Prepare a vector with clones of high priority transactions found in the mempool
661        let mempool = self.mempool.read();
662        let transaction_ids = mempool.all_transaction_ids_with_priority(Priority::High);
663        if transaction_ids.is_empty() {
664            debug!("<> Revalidating high priority transactions found no transactions");
665            return;
666        } else {
667            debug!("<> Revalidating {} high priority transactions...", transaction_ids.len());
668        }
669        drop(mempool);
670        // read lock on mempool by transaction chunks
671        let mut transactions = Vec::with_capacity(transaction_ids.len());
672        for chunk in &transaction_ids.iter().chunks(TRANSACTION_CHUNK_SIZE) {
673            let mempool = self.mempool.read();
674            transactions.extend(chunk.filter_map(|x| mempool.get_transaction(x, TransactionQuery::TransactionsOnly)));
675        }
676
677        let mut valid: usize = 0;
678        let mut accepted: usize = 0;
679        let mut other: usize = 0;
680        let mut missing_outpoint: usize = 0;
681        let mut invalid: usize = 0;
682
683        // We process the transactions by level of dependency inside the batch.
684        // Doing so allows to remove all chained dependencies of rejected transactions.
685        let _swo = Stopwatch::<800>::with_threshold("revalidate topological_sort op");
686        let sorted_transactions = transactions.topological_into_iter();
687        drop(_swo);
688
689        // read lock on mempool by transaction chunks
690        // As the revalidation process is no longer atomic, we filter the transactions ready for revalidation,
691        // keeping only the ones actually present in the mempool (see comment above).
692        let _swo = Stopwatch::<900>::with_threshold("revalidate populate_mempool_entries op");
693        let mut transactions = Vec::with_capacity(sorted_transactions.len());
694        for chunk in &sorted_transactions.chunks(TRANSACTION_CHUNK_SIZE) {
695            let mempool = self.mempool.read();
696            let txs = chunk.filter_map(|mut x| {
697                let transaction_id = x.id();
698                if mempool.has_accepted_transaction(&transaction_id) {
699                    #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
700                    {
701                        accepted += 1;
702                    }
703                    None
704                } else if mempool.has_transaction(&transaction_id, TransactionQuery::TransactionsOnly) {
705                    x.clear_entries();
706
707                    match mempool.populate_mempool_entries(&mut x) {
708                        Ok(()) => {}
709                        Err(PopulateError::ImpossibleOutpoint) => {
710                            // Impossible for txs admitted to the pool: they were fully populated before insertion.
711                            // Fall through to the default missing-outpoints path.
712                        }
713                    }
714
715                    match x.is_fully_populated() {
716                        false => Some(x),
717                        true => {
718                            // If all entries are populated with mempool UTXOs, we already know the transaction is valid
719                            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
720                            {
721                                valid += 1;
722                            }
723                            None
724                        }
725                    }
726                } else {
727                    #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
728                    {
729                        other += 1;
730                    }
731                    None
732                }
733            });
734            transactions.extend(txs);
735        }
736        drop(_swo);
737
738        // no lock on mempool
739        // We process the transactions by chunks of max block mass to prevent locking the virtual processor for too long.
740        let mut lower_bound: usize = 0;
741        let mut validation_results = Vec::with_capacity(transactions.len());
742        while let Some(upper_bound) = self.next_transaction_chunk_upper_bound(&transactions, lower_bound) {
743            assert!(lower_bound < upper_bound, "the chunk is never empty");
744            let _swo = Stopwatch::<60>::with_threshold("revalidate validate_mempool_transactions_in_parallel op");
745            validation_results
746                .extend(populate_mempool_transactions_in_parallel(consensus, &mut transactions[lower_bound..upper_bound]));
747            drop(_swo);
748            lower_bound = upper_bound;
749        }
750        assert_eq!(transactions.len(), validation_results.len(), "every transaction should have a matching validation result");
751
752        // write lock on mempool
753        // Depending on the validation result, transactions are either accepted or removed
754        for chunk in &transactions.into_iter().zip(validation_results).chunks(TRANSACTION_CHUNK_SIZE) {
755            let mut valid_ids = Vec::with_capacity(TRANSACTION_CHUNK_SIZE);
756            let mut mempool = self.mempool.write();
757            let _swo = Stopwatch::<60>::with_threshold("revalidate update_revalidated_transaction op");
758            for (transaction, validation_result) in chunk {
759                let transaction_id = transaction.id();
760                match validation_result {
761                    Ok(()) => {
762                        // Only consider transactions still being in the mempool since during the validation some might have been removed.
763                        if mempool.update_revalidated_transaction(transaction) {
764                            // A following transaction should not remove this one from the pool since we process in a topological order.
765                            // Still, considering the (very unlikely) scenario of two high priority txs sandwiching a low one, where
766                            // in this case topological order is not guaranteed since we only considered chained dependencies of
767                            // high-priority transactions, we might wrongfully return as valid the id of a removed transaction.
768                            // However, as only consequence, said transaction would then be advertised to registered peers and not be
769                            // provided upon request.
770                            valid_ids.push(transaction_id);
771                            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
772                            {
773                                valid += 1;
774                            }
775                        } else {
776                            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
777                            {
778                                other += 1;
779                            }
780                        }
781                    }
782                    Err(RuleError::RejectMissingOutpoint) => {
783                        let missing_txs = transaction
784                            .entries
785                            .iter()
786                            .zip(transaction.tx.inputs.iter())
787                            .filter_map(|(entry, input)| entry.is_none().then_some(input.previous_outpoint.transaction_id))
788                            .collect::<Vec<_>>();
789
790                        // A transaction may have missing outpoints for legitimate reasons related to concurrency, like a race condition between
791                        // an accepted block having not started yet or unfinished call to handle_new_block_transactions but already processed by
792                        // the consensus and this ongoing call to revalidate.
793                        //
794                        // So we only remove the transaction and keep its redeemers in the mempool because we cannot be sure they are invalid, in
795                        // fact in the race condition case they are valid regarding outpoints.
796                        let extra_info = match missing_txs.len() {
797                            0 => " but no missing tx!".to_string(), // this is never supposed to happen
798                            1 => format!(" missing tx {}", missing_txs[0]),
799                            n => format!(" with {} missing txs {}..{}", n, missing_txs[0], missing_txs.last().unwrap()),
800                        };
801
802                        // This call cleanly removes the invalid transaction.
803                        _ = mempool
804                            .remove_transaction(
805                                &transaction_id,
806                                false,
807                                TxRemovalReason::RevalidationWithMissingOutpoints,
808                                extra_info.as_str(),
809                            )
810                            .inspect_err(|err| warn!("Failed to remove transaction {} from mempool: {}", transaction_id, err));
811                        #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
812                        {
813                            missing_outpoint += 1;
814                        }
815                    }
816                    Err(err) => {
817                        // Rust rewrite note:
818                        // The behavior changes here compared to the golang version.
819                        // The failed revalidation is simply logged and the process continues.
820                        warn!(
821                            "Removing high priority transaction {0} and its redeemers, it failed revalidation with {1}",
822                            transaction_id, err
823                        );
824                        // This call cleanly removes the invalid transaction and its redeemers.
825                        _ = mempool
826                            .remove_transaction(&transaction_id, true, TxRemovalReason::Muted, "")
827                            .inspect_err(|err| warn!("Failed to remove transaction {} from mempool: {}", transaction_id, err));
828                        #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
829                        {
830                            invalid += 1;
831                        }
832                    }
833                }
834            }
835            if !valid_ids.is_empty() {
836                let _ = transaction_ids_sender.send(valid_ids);
837            }
838            drop(_swo);
839            drop(mempool);
840        }
841        #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
842        let removed_count = accepted + missing_outpoint + invalid;
843        match removed_count {
844            0 => {
845                info!("Revalidated {} high priority transactions", valid);
846            }
847            _ => {
848                info!(
849                    "Revalidated {} and removed {} high priority transactions (removals: {} accepted, {} missing outpoint, {} invalid)",
850                    valid, removed_count, accepted, missing_outpoint, invalid,
851                );
852                if other > 0 {
853                    debug!(
854                        "During revalidation of high priority transactions {} txs were removed from the mempool by concurrent flows",
855                        other
856                    )
857                }
858            }
859        }
860    }
861
862    pub fn has_accepted_transaction(&self, transaction_id: &TransactionId) -> bool {
863        self.mempool.read().has_accepted_transaction(transaction_id)
864    }
865
866    pub fn unaccepted_transactions(&self, transactions: Vec<TransactionId>) -> Vec<TransactionId> {
867        self.mempool.read().unaccepted_transactions(transactions)
868    }
869
870    pub fn unknown_transactions(&self, transactions: Vec<TransactionId>) -> Vec<TransactionId> {
871        self.mempool.read().unknown_transactions(transactions)
872    }
873
874    #[cfg(test)]
875    pub(crate) fn get_estimated_size(&self) -> usize {
876        self.mempool.read().get_estimated_size()
877    }
878}
879
880/// Async proxy for the mining manager
881#[derive(Clone)]
882pub struct MiningManagerProxy {
883    inner: Arc<MiningManager>,
884}
885
886impl MiningManagerProxy {
887    pub fn new(inner: Arc<MiningManager>) -> Self {
888        Self { inner }
889    }
890
891    pub async fn get_block_template(self, consensus: &ConsensusProxy, miner_data: MinerData) -> MiningManagerResult<BlockTemplate> {
892        consensus.clone().spawn_blocking(move |c| self.inner.get_block_template(c, &miner_data)).await
893    }
894
895    /// Returns realtime feerate estimations based on internal mempool state
896    pub async fn get_realtime_feerate_estimations(self) -> FeerateEstimations {
897        spawn_blocking(move || self.inner.get_realtime_feerate_estimations()).await.unwrap()
898    }
899
900    /// Returns realtime feerate estimations based on internal mempool state with additional verbose data
901    pub async fn get_realtime_feerate_estimations_verbose(
902        self,
903        consensus: &ConsensusProxy,
904        prefix: kaspa_addresses::Prefix,
905    ) -> MiningManagerResult<FeeEstimateVerbose> {
906        consensus.clone().spawn_blocking(move |c| self.inner.get_realtime_feerate_estimations_verbose(c, prefix)).await
907    }
908
909    /// Validates a transaction and adds it to the set of known transactions that have not yet been
910    /// added to any block.
911    ///
912    /// The validation is constrained by a Replace by fee policy applied
913    /// to double spends in the mempool. For more information, see [`RbfPolicy`].
914    ///
915    /// The returned transactions are references of objects owned by the mempool.
916    pub async fn validate_and_insert_transaction(
917        self,
918        consensus: &ConsensusProxy,
919        transaction: Transaction,
920        priority: Priority,
921        orphan: Orphan,
922        rbf_policy: RbfPolicy,
923    ) -> MiningManagerResult<TransactionInsertion> {
924        consensus
925            .clone()
926            .spawn_blocking(move |c| self.inner.validate_and_insert_transaction(c, transaction, priority, orphan, rbf_policy))
927            .await
928    }
929
930    /// Validates a batch of transactions, handling iteratively only the independent ones, and
931    /// adds those to the set of known transactions that have not yet been added to any block.
932    ///
933    /// The validation is constrained by a Replace by fee policy applied
934    /// to double spends in the mempool. For more information, see [`RbfPolicy`].
935    ///
936    /// Returns transactions that where unorphaned following the insertion of the provided
937    /// transactions. The returned transactions are references of objects owned by the mempool.
938    pub async fn validate_and_insert_transaction_batch(
939        self,
940        consensus: &ConsensusProxy,
941        transactions: Vec<Transaction>,
942        priority: Priority,
943        orphan: Orphan,
944        rbf_policy: RbfPolicy,
945    ) -> Vec<MiningManagerResult<Arc<Transaction>>> {
946        consensus
947            .clone()
948            .spawn_blocking(move |c| self.inner.validate_and_insert_transaction_batch(c, transactions, priority, orphan, rbf_policy))
949            .await
950    }
951
952    pub async fn handle_new_block_transactions(
953        self,
954        consensus: &ConsensusProxy,
955        block_daa_score: u64,
956        block_transactions: Arc<Vec<Transaction>>,
957    ) -> MiningManagerResult<Vec<Arc<Transaction>>> {
958        consensus
959            .clone()
960            .spawn_blocking(move |c| self.inner.handle_new_block_transactions(c, block_daa_score, &block_transactions))
961            .await
962    }
963
964    pub async fn expire_low_priority_transactions(self, consensus: &ConsensusProxy) {
965        consensus.clone().spawn_blocking(move |c| self.inner.expire_low_priority_transactions(c)).await;
966    }
967
968    pub async fn revalidate_high_priority_transactions(
969        self,
970        consensus: &ConsensusProxy,
971        transaction_ids_sender: UnboundedSender<Vec<TransactionId>>,
972    ) {
973        consensus.clone().spawn_blocking(move |c| self.inner.revalidate_high_priority_transactions(c, transaction_ids_sender)).await;
974    }
975
976    /// Try to return a mempool transaction by its id.
977    ///
978    /// Note: the transaction is an orphan if tx.is_fully_populated() returns false.
979    pub async fn get_transaction(self, transaction_id: TransactionId, query: TransactionQuery) -> Option<MutableTransaction> {
980        spawn_blocking(move || self.inner.get_transaction(&transaction_id, query)).await.unwrap()
981    }
982
983    /// Returns whether the mempool holds this transaction in any form.
984    pub async fn has_transaction(self, transaction_id: TransactionId, query: TransactionQuery) -> bool {
985        spawn_blocking(move || self.inner.has_transaction(&transaction_id, query)).await.unwrap()
986    }
987
988    pub async fn transaction_count(self, query: TransactionQuery) -> usize {
989        spawn_blocking(move || self.inner.transaction_count(query)).await.unwrap()
990    }
991
992    pub async fn get_all_transactions(self, query: TransactionQuery) -> (Vec<MutableTransaction>, Vec<MutableTransaction>) {
993        spawn_blocking(move || self.inner.get_all_transactions(query)).await.unwrap()
994    }
995
996    /// get_transactions_by_addresses returns the sending and receiving transactions for
997    /// a set of addresses.
998    ///
999    /// Note: a transaction is an orphan if tx.is_fully_populated() returns false.
1000    pub async fn get_transactions_by_addresses(
1001        self,
1002        script_public_keys: ScriptPublicKeySet,
1003        query: TransactionQuery,
1004    ) -> GroupedOwnerTransactions {
1005        spawn_blocking(move || self.inner.get_transactions_by_addresses(&script_public_keys, query)).await.unwrap()
1006    }
1007
1008    /// Returns whether a transaction id was registered as accepted in the mempool, meaning
1009    /// that the consensus accepted a block containing it and said block was handled by the
1010    /// mempool.
1011    ///
1012    /// Registered transaction ids expire after a delay and are unregistered from the mempool.
1013    /// So a returned value of true means with certitude that the transaction was accepted and
1014    /// a false means either the transaction was never accepted or it was but beyond the expiration
1015    /// delay.
1016    pub async fn has_accepted_transaction(self, transaction_id: TransactionId) -> bool {
1017        spawn_blocking(move || self.inner.has_accepted_transaction(&transaction_id)).await.unwrap()
1018    }
1019
1020    /// Returns a vector of unaccepted transactions.
1021    /// For more details, see [`Self::has_accepted_transaction()`].
1022    pub async fn unaccepted_transactions(self, transactions: Vec<TransactionId>) -> Vec<TransactionId> {
1023        spawn_blocking(move || self.inner.unaccepted_transactions(transactions)).await.unwrap()
1024    }
1025
1026    /// Returns a vector with all transaction ids that are neither in the mempool, nor in the orphan pool
1027    /// nor accepted.
1028    pub async fn unknown_transactions(self, transactions: Vec<TransactionId>) -> Vec<TransactionId> {
1029        spawn_blocking(move || self.inner.unknown_transactions(transactions)).await.unwrap()
1030    }
1031
1032    pub fn snapshot(&self) -> MempoolCountersSnapshot {
1033        self.inner.counters.snapshot()
1034    }
1035
1036    pub fn p2p_tx_count_sample(&self) -> P2pTxCountSample {
1037        self.inner.counters.p2p_tx_count_sample()
1038    }
1039
1040    /// Returns a recent sample of transaction count which is not necessarily accurate
1041    /// but is updated enough for being used as a stats/metric
1042    pub fn transaction_count_sample(&self, query: TransactionQuery) -> u64 {
1043        let mut count = 0;
1044        if query.include_transaction_pool() {
1045            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
1046            {
1047                count += self.inner.counters.txs_sample.load(std::sync::atomic::Ordering::Relaxed)
1048            }
1049        }
1050        if query.include_orphan_pool() {
1051            #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
1052            {
1053                count += self.inner.counters.orphans_sample.load(std::sync::atomic::Ordering::Relaxed)
1054            }
1055        }
1056        count
1057    }
1058}
1059
1060/// Represents statistical information about fee rates of transactions.
1061struct Stats {
1062    /// The maximum fee rate observed.
1063    max: f64,
1064    /// The median fee rate observed.
1065    median: f64,
1066    /// The minimum fee rate observed.
1067    min: f64,
1068}
1069/// Calculates the maximum, median, and minimum fee rates (fee per unit mass)
1070/// for a set of transactions, excluding the first transaction which is assumed
1071/// to be the coinbase transaction.
1072///
1073/// # Arguments
1074///
1075/// * `transactions` - A vector of `Transaction` objects. The first transaction
1076///   is assumed to be the coinbase transaction and is excluded from fee rate
1077///   calculations.
1078/// * `calculated_fees` - A vector of fees associated with the transactions.
1079///   This vector should have one less element than the `transactions` vector
1080///   since the first transaction (coinbase) does not have a fee.
1081///
1082/// # Returns
1083///
1084/// Returns an `Option<Stats>` containing the maximum, median, and minimum fee
1085/// rates if the input vectors are valid. Returns `None` if the vectors are
1086/// empty or if the lengths are inconsistent.
1087fn feerate_stats(transactions: Vec<Transaction>, calculated_fees: Vec<u64>) -> Option<Stats> {
1088    if calculated_fees.is_empty() {
1089        return None;
1090    }
1091    #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(LENGTH)")]
1092    if transactions.len() != calculated_fees.len() + 1 {
1093        error!(
1094            "[feerate_stats] block template transactions length ({}) is expected to be one more than `calculated_fees` length ({})",
1095            transactions.len(),
1096            calculated_fees.len()
1097        );
1098        return None;
1099    }
1100    debug_assert!(transactions[0].is_coinbase());
1101    let mut feerates = calculated_fees
1102        .into_iter()
1103        .zip(transactions
1104            .iter()
1105            // skip coinbase tx
1106            .skip(1)
1107            .map(Transaction::storage_mass))
1108        .map(|(fee, mass)| fee as f64 / mass as f64)
1109        .collect_vec();
1110    feerates.sort_unstable_by(f64::total_cmp);
1111
1112    #[allow(clippy::arithmetic_side_effects, reason = "Empty fees is checked above.")]
1113    let max = feerates[feerates.len() - 1];
1114    let min = feerates[0];
1115    let median = feerates[feerates.len() / 2];
1116
1117    Some(Stats { max, median, min })
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123    use kaspa_consensus_core::subnets;
1124    use std::iter::repeat_n;
1125
1126    fn transactions(length: usize) -> Vec<Transaction> {
1127        let tx = || {
1128            let tx = Transaction::new(0, vec![], vec![], 0, Default::default(), 0, vec![]);
1129            tx.set_storage_mass(2);
1130            tx
1131        };
1132        let mut txs = repeat_n(tx(), length).collect_vec();
1133        txs[0].subnetwork_id = subnets::SUBNETWORK_ID_COINBASE;
1134        txs
1135    }
1136
1137    #[test]
1138    fn feerate_stats_test() {
1139        let calculated_fees = vec![100u64, 200, 300, 400];
1140        let txs = transactions(calculated_fees.len() + 1);
1141        let Stats { max, median, min } = feerate_stats(txs, calculated_fees).unwrap();
1142        assert_eq!(max, 200.0);
1143        assert_eq!(median, 150.0);
1144        assert_eq!(min, 50.0);
1145    }
1146
1147    #[test]
1148    fn feerate_stats_empty_test() {
1149        let calculated_fees = vec![];
1150        let txs = transactions(calculated_fees.len() + 1);
1151        assert!(feerate_stats(txs, calculated_fees).is_none());
1152    }
1153
1154    #[test]
1155    fn feerate_stats_inconsistent_test() {
1156        let calculated_fees = vec![100u64, 200, 300, 400];
1157        let txs = transactions(calculated_fees.len());
1158        assert!(feerate_stats(txs, calculated_fees).is_none());
1159    }
1160}