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 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 let block_template = BlockTemplateBuilder::modify_block_template(consensus, miner_data, &immutable_template)?;
100
101 return Ok(block_template);
104 }
105
106 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 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 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 pub(crate) fn build_selector(&self) -> Box<dyn TemplateTransactionSelector> {
217 self.mempool.read().build_selector()
218 }
219
220 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 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 {
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 #[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 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 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 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 let validation_result = validate_mempool_transaction(consensus, &mut transaction, &args);
323 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 #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(LENGTH)")]
332 let mut accepted_transactions = Vec::with_capacity(unorphaned_transactions.len() + 1);
333 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 let mut accepted_transactions = Vec::with_capacity(incoming_transactions.len());
351 let args = TransactionValidationBatchArgs::new();
354 while !incoming_transactions.is_empty() {
357 let (mut transactions, priorities): (Vec<MutableTransaction>, Vec<Priority>) =
360 incoming_transactions.into_iter().map(|x| (x.mtx, x.priority)).unzip();
361
362 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 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 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 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 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 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 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 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 mass = mass.saturating_add(tx.calculated_non_contextual_masses.unwrap().normalized_max(&cofactors));
538 mass >= cofactors.reference
539 })
540 .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 pub fn get_transaction(&self, transaction_id: &TransactionId, query: TransactionQuery) -> Option<MutableTransaction> {
556 self.mempool.read().get_transaction(transaction_id, query)
557 }
558
559 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 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 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 pub fn get_transactions_by_addresses(
592 &self,
593 script_public_keys: &ScriptPublicKeySet,
594 query: TransactionQuery,
595 ) -> GroupedOwnerTransactions {
596 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 let unorphaned_transactions = self.mempool.write().handle_new_block_transactions(block_daa_score, block_transactions)?;
616
617 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 debug!("<> Expiring low priority transactions...");
626
627 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 self.mempool.write().expire_accepted_transactions(consensus);
634
635 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 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 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 let _swo = Stopwatch::<800>::with_threshold("revalidate topological_sort op");
686 let sorted_transactions = transactions.topological_into_iter();
687 drop(_swo);
688
689 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 }
713 }
714
715 match x.is_fully_populated() {
716 false => Some(x),
717 true => {
718 #[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 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 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 if mempool.update_revalidated_transaction(transaction) {
764 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 let extra_info = match missing_txs.len() {
797 0 => " but no missing tx!".to_string(), 1 => format!(" missing tx {}", missing_txs[0]),
799 n => format!(" with {} missing txs {}..{}", n, missing_txs[0], missing_txs.last().unwrap()),
800 };
801
802 _ = 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 warn!(
821 "Removing high priority transaction {0} and its redeemers, it failed revalidation with {1}",
822 transaction_id, err
823 );
824 _ = 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#[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 pub async fn get_realtime_feerate_estimations(self) -> FeerateEstimations {
897 spawn_blocking(move || self.inner.get_realtime_feerate_estimations()).await.unwrap()
898 }
899
900 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 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 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 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 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 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 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 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 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 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
1060struct Stats {
1062 max: f64,
1064 median: f64,
1066 min: f64,
1068}
1069fn 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(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}