diff --git a/Cargo.lock b/Cargo.lock index 0d4703b8ad..9b64388ee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3303,6 +3303,7 @@ dependencies = [ "kaspa-utils-tower", "kaspa-utxoindex", "log", + "portable-atomic", "tokio", "triggered", "workflow-rpc", diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index c849154800..5fb94f4f7e 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -229,6 +229,10 @@ impl Rpc { } } } + RpcApiOps::GetFeeInfo => { + let result = rpc.get_fee_info_call(GetFeeInfoRequest {}).await; + self.println(&ctx, result); + } _ => { tprintln!(ctx, "rpc method exists but is not supported by the cli: '{op_str}'\r\n"); return Ok(()); diff --git a/consensus/core/src/block.rs b/consensus/core/src/block.rs index dde6fd5e75..f0f956fea4 100644 --- a/consensus/core/src/block.rs +++ b/consensus/core/src/block.rs @@ -105,6 +105,8 @@ pub struct BlockTemplate { pub selected_parent_timestamp: u64, pub selected_parent_daa_score: u64, pub selected_parent_hash: Hash, + /// length one less than txs length due to lack of coinbase transaction + pub calculated_fees: Vec, } impl BlockTemplate { @@ -115,8 +117,17 @@ impl BlockTemplate { selected_parent_timestamp: u64, selected_parent_daa_score: u64, selected_parent_hash: Hash, + calculated_fees: Vec, ) -> Self { - Self { block, miner_data, coinbase_has_red_reward, selected_parent_timestamp, selected_parent_daa_score, selected_parent_hash } + Self { + block, + miner_data, + coinbase_has_red_reward, + selected_parent_timestamp, + selected_parent_daa_score, + selected_parent_hash, + calculated_fees, + } } pub fn to_virtual_state_approx_id(&self) -> VirtualStateApproxId { diff --git a/consensus/src/pipeline/virtual_processor/processor.rs b/consensus/src/pipeline/virtual_processor/processor.rs index ded0622514..5310d5ab4b 100644 --- a/consensus/src/pipeline/virtual_processor/processor.rs +++ b/consensus/src/pipeline/virtual_processor/processor.rs @@ -78,6 +78,7 @@ use kaspa_notify::{events::EventType, notifier::Notify}; use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}; use itertools::Itertools; +use kaspa_consensus_core::tx::ValidatedTransaction; use kaspa_utils::binary_heap::BinaryHeapExtensions; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use rand::{seq::SliceRandom, Rng}; @@ -821,12 +822,9 @@ impl VirtualStateProcessor { txs: &[Transaction], virtual_state: &VirtualState, utxo_view: &V, - ) -> Vec> { - self.thread_pool.install(|| { - txs.par_iter() - .map(|tx| self.validate_block_template_transaction(tx, virtual_state, &utxo_view)) - .collect::>>() - }) + ) -> Vec> { + self.thread_pool + .install(|| txs.par_iter().map(|tx| self.validate_block_template_transaction(tx, virtual_state, &utxo_view)).collect()) } fn validate_block_template_transaction( @@ -834,13 +832,14 @@ impl VirtualStateProcessor { tx: &Transaction, virtual_state: &VirtualState, utxo_view: &impl UtxoView, - ) -> TxResult<()> { + ) -> TxResult { // No need to validate the transaction in isolation since we rely on the mining manager to submit transactions // which were previously validated through `validate_mempool_transaction_and_populate`, hence we only perform // in-context validations self.transaction_validator.utxo_free_tx_validation(tx, virtual_state.daa_score, virtual_state.past_median_time)?; - self.validate_transaction_in_utxo_context(tx, utxo_view, virtual_state.daa_score, TxValidationFlags::Full)?; - Ok(()) + let ValidatedTransaction { calculated_fee, .. } = + self.validate_transaction_in_utxo_context(tx, utxo_view, virtual_state.daa_score, TxValidationFlags::Full)?; + Ok(calculated_fee) } pub fn build_block_template( @@ -863,11 +862,17 @@ impl VirtualStateProcessor { let virtual_utxo_view = &virtual_read.utxo_set; let mut invalid_transactions = HashMap::new(); + let mut calculated_fees = Vec::with_capacity(txs.len()); let results = self.validate_block_template_transactions_in_parallel(&txs, &virtual_state, &virtual_utxo_view); for (tx, res) in txs.iter().zip(results) { - if let Err(e) = res { - invalid_transactions.insert(tx.id(), e); - tx_selector.reject_selection(tx.id()); + match res { + Err(e) => { + invalid_transactions.insert(tx.id(), e); + tx_selector.reject_selection(tx.id()); + } + Ok(fee) => { + calculated_fees.push(fee); + } } } @@ -882,12 +887,16 @@ impl VirtualStateProcessor { let next_batch_results = self.validate_block_template_transactions_in_parallel(&next_batch, &virtual_state, &virtual_utxo_view); for (tx, res) in next_batch.into_iter().zip(next_batch_results) { - if let Err(e) = res { - invalid_transactions.insert(tx.id(), e); - tx_selector.reject_selection(tx.id()); - has_rejections = true; - } else { - txs.push(tx); + match res { + Err(e) => { + invalid_transactions.insert(tx.id(), e); + tx_selector.reject_selection(tx.id()); + has_rejections = true; + } + Ok(fee) => { + txs.push(tx); + calculated_fees.push(fee); + } } } } @@ -904,7 +913,7 @@ impl VirtualStateProcessor { drop(virtual_read); // Build the template - self.build_block_template_from_virtual_state(virtual_state, miner_data, txs) + self.build_block_template_from_virtual_state(virtual_state, miner_data, txs, calculated_fees) } pub(crate) fn validate_block_template_transactions( @@ -932,6 +941,7 @@ impl VirtualStateProcessor { virtual_state: Arc, miner_data: MinerData, mut txs: Vec, + calculated_fees: Vec, ) -> Result { // [`calc_block_parents`] can use deep blocks below the pruning point for this calculation, so we // need to hold the pruning lock. @@ -985,6 +995,7 @@ impl VirtualStateProcessor { selected_parent_timestamp, selected_parent_daa_score, selected_parent_hash, + calculated_fees, )) } diff --git a/consensus/src/pipeline/virtual_processor/test_block_builder.rs b/consensus/src/pipeline/virtual_processor/test_block_builder.rs index 872bf15b40..2654a6a5fe 100644 --- a/consensus/src/pipeline/virtual_processor/test_block_builder.rs +++ b/consensus/src/pipeline/virtual_processor/test_block_builder.rs @@ -61,6 +61,6 @@ impl TestBlockBuilder { let pov_virtual_utxo_view = (&virtual_read.utxo_set).compose(accumulated_diff); self.validate_block_template_transactions(&txs, &pov_virtual_state, &pov_virtual_utxo_view)?; drop(virtual_read); - self.build_block_template_from_virtual_state(pov_virtual_state, miner_data, txs) + self.build_block_template_from_virtual_state(pov_virtual_state, miner_data, txs, vec![]) } } diff --git a/mining/src/lib.rs b/mining/src/lib.rs index 2986577efe..7094772437 100644 --- a/mining/src/lib.rs +++ b/mining/src/lib.rs @@ -33,6 +33,8 @@ pub struct MiningCounters { pub txs_sample: AtomicU64, pub orphans_sample: AtomicU64, pub accepted_sample: AtomicU64, + + pub total_mass: AtomicU64, } impl Default for MiningCounters { @@ -49,6 +51,7 @@ impl Default for MiningCounters { txs_sample: Default::default(), orphans_sample: Default::default(), accepted_sample: Default::default(), + total_mass: Default::default(), } } } @@ -67,6 +70,7 @@ impl MiningCounters { txs_sample: self.txs_sample.load(Ordering::Relaxed), orphans_sample: self.orphans_sample.load(Ordering::Relaxed), accepted_sample: self.accepted_sample.load(Ordering::Relaxed), + total_mass: self.total_mass.load(Ordering::Relaxed), } } @@ -102,6 +106,7 @@ pub struct MempoolCountersSnapshot { pub txs_sample: u64, pub orphans_sample: u64, pub accepted_sample: u64, + pub total_mass: u64, } impl MempoolCountersSnapshot { @@ -157,6 +162,7 @@ impl core::ops::Sub for &MempoolCountersSnapshot { txs_sample: (self.txs_sample + rhs.txs_sample) / 2, orphans_sample: (self.orphans_sample + rhs.orphans_sample) / 2, accepted_sample: (self.accepted_sample + rhs.accepted_sample) / 2, + total_mass: self.total_mass.checked_sub(rhs.total_mass).unwrap_or_default(), } } } diff --git a/mining/src/mempool/remove_transaction.rs b/mining/src/mempool/remove_transaction.rs index 960ebc264b..cfb6acd0e4 100644 --- a/mining/src/mempool/remove_transaction.rs +++ b/mining/src/mempool/remove_transaction.rs @@ -6,6 +6,7 @@ use crate::mempool::{ use kaspa_consensus_core::tx::TransactionId; use kaspa_core::{debug, warn}; use kaspa_utils::iter::IterExtensions; +use std::sync::atomic::Ordering; impl Mempool { pub(crate) fn remove_transaction( @@ -33,6 +34,7 @@ impl Mempool { for tx_id in removed_transactions.iter() { // Remove the tx from the transaction pool and the UTXO set (handled within the pool) let tx = self.transaction_pool.remove_transaction(tx_id)?; + self.counters.total_mass.fetch_sub(tx.mtx.tx.mass(), Ordering::Relaxed); // Update/remove descendent orphan txs (depending on `remove_redeemers`) let txs = self.orphan_pool.update_orphans_after_transaction_removed(&tx, remove_redeemers)?; removed_orphans.extend(txs.into_iter().map(|x| x.id())); diff --git a/mining/src/mempool/validate_and_insert_transaction.rs b/mining/src/mempool/validate_and_insert_transaction.rs index 591fa5c4aa..6ce246d6a7 100644 --- a/mining/src/mempool/validate_and_insert_transaction.rs +++ b/mining/src/mempool/validate_and_insert_transaction.rs @@ -13,6 +13,7 @@ use kaspa_consensus_core::{ tx::{MutableTransaction, Transaction, TransactionId, TransactionOutpoint, UtxoEntry}, }; use kaspa_core::{debug, info}; +use std::sync::atomic::Ordering; use std::sync::Arc; impl Mempool { @@ -78,6 +79,7 @@ impl Mempool { // Add the transaction to the mempool as a MempoolTransaction and return a clone of the embedded Arc let accepted_transaction = self.transaction_pool.add_transaction(transaction, consensus.get_virtual_daa_score(), priority)?.mtx.tx.clone(); + self.counters.total_mass.fetch_add(accepted_transaction.mass(), Ordering::Relaxed); Ok(Some(accepted_transaction)) } diff --git a/mining/src/testutils/consensus_mock.rs b/mining/src/testutils/consensus_mock.rs index 94d774c428..9e94e81179 100644 --- a/mining/src/testutils/consensus_mock.rs +++ b/mining/src/testutils/consensus_mock.rs @@ -100,7 +100,7 @@ impl ConsensusApi for ConsensusMock { ); let mutable_block = MutableBlock::new(header, txs); - Ok(BlockTemplate::new(mutable_block, miner_data, coinbase.has_red_reward, now, 0, ZERO_HASH)) + Ok(BlockTemplate::new(mutable_block, miner_data, coinbase.has_red_reward, now, 0, ZERO_HASH, vec![])) } fn validate_mempool_transaction(&self, mutable_tx: &mut MutableTransaction) -> TxResult<()> { diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index a02fbf7462..66460aafa5 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -86,6 +86,8 @@ pub enum RpcApiOps { GetCoinSupply, /// Get DAA Score timestamp estimate GetDaaScoreTimestampEstimate, + /// Get priority fee estimate + GetFeeInfo, // Subscription commands for starting/stopping notifications NotifyBlockAdded, diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index 36f8ef3085..78c60188de 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -304,6 +304,11 @@ pub trait RpcApi: Sync + Send + AnySync { request: GetDaaScoreTimestampEstimateRequest, ) -> RpcResult; + async fn get_fee_info(&self) -> RpcResult { + self.get_fee_info_call(GetFeeInfoRequest {}).await + } + async fn get_fee_info_call(&self, request: GetFeeInfoRequest) -> RpcResult; + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index 7366bf3ccb..9f715938aa 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -825,6 +825,37 @@ impl GetDaaScoreTimestampEstimateResponse { } } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFeeInfoRequest {} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct VirtualFeePerMass { + pub max: f64, + pub median: f64, + pub min: f64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub enum FeePerMass { + VirtualFeePerMass(VirtualFeePerMass), +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFeeInfoResponse { + pub fee_per_mass: FeePerMass, + pub mempool_total_mass: u64, +} + +impl GetFeeInfoResponse { + pub fn new(fee_per_mass: FeePerMass, mempool_total_mass: u64) -> Self { + Self { fee_per_mass, mempool_total_mass } + } +} + // ---------------------------------------------------------------------------- // Subscriptions & notifications // ---------------------------------------------------------------------------- diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index c7eebd8d1e..222094c775 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -271,6 +271,7 @@ impl RpcApi for GrpcClient { route!(get_mempool_entries_by_addresses_call, GetMempoolEntriesByAddresses); route!(get_coin_supply_call, GetCoinSupply); route!(get_daa_score_timestamp_estimate_call, GetDaaScoreTimestampEstimate); + route!(get_fee_info_call, GetFeeInfo); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/grpc/core/proto/messages.proto b/rpc/grpc/core/proto/messages.proto index ec72426350..d2f0908623 100644 --- a/rpc/grpc/core/proto/messages.proto +++ b/rpc/grpc/core/proto/messages.proto @@ -59,6 +59,7 @@ message KaspadRequest { GetServerInfoRequestMessage getServerInfoRequest = 1092; GetSyncStatusRequestMessage getSyncStatusRequest = 1094; GetDaaScoreTimestampEstimateRequestMessage GetDaaScoreTimestampEstimateRequest = 1096; + GetFeeInfoRequestMessage getFeeInfoRequest = 1098; } } @@ -118,6 +119,7 @@ message KaspadResponse { GetServerInfoResponseMessage getServerInfoResponse = 1093; GetSyncStatusResponseMessage getSyncStatusResponse = 1095; GetDaaScoreTimestampEstimateResponseMessage GetDaaScoreTimestampEstimateResponse = 1097; + GetFeeInfoResponseMessage getFeeInfoResponse = 1099; } } diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index e558c65485..a4160755fa 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -851,3 +851,19 @@ message GetDaaScoreTimestampEstimateResponseMessage{ repeated uint64 timestamps = 1; RPCError error = 1000; } + +message GetFeeInfoRequestMessage {} + +message GetFeeInfoResponseMessage { + oneof fee_per_mass { + Virtual virtual = 2; // 1 is reserved for `All` + } + uint64 mempool_total_mass = 11; + RPCError error = 1000; +} + +message Virtual { + double max = 1; + double median = 2; + double min = 3; +} diff --git a/rpc/grpc/core/src/convert/kaspad.rs b/rpc/grpc/core/src/convert/kaspad.rs index 0fef61523a..ef9802bb3a 100644 --- a/rpc/grpc/core/src/convert/kaspad.rs +++ b/rpc/grpc/core/src/convert/kaspad.rs @@ -57,6 +57,7 @@ pub mod kaspad_request_convert { impl_into_kaspad_request!(GetServerInfo); impl_into_kaspad_request!(GetSyncStatus); impl_into_kaspad_request!(GetDaaScoreTimestampEstimate); + impl_into_kaspad_request!(GetFeeInfo); impl_into_kaspad_request!(NotifyBlockAdded); impl_into_kaspad_request!(NotifyNewBlockTemplate); @@ -188,6 +189,7 @@ pub mod kaspad_response_convert { impl_into_kaspad_response!(GetServerInfo); impl_into_kaspad_response!(GetSyncStatus); impl_into_kaspad_response!(GetDaaScoreTimestampEstimate); + impl_into_kaspad_response!(GetFeeInfo); impl_into_kaspad_notify_response!(NotifyBlockAdded); impl_into_kaspad_notify_response!(NotifyNewBlockTemplate); diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 9babf29c84..0043e79124 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -23,8 +23,8 @@ use kaspa_consensus_core::network::NetworkId; use kaspa_core::debug; use kaspa_notify::subscription::Command; use kaspa_rpc_core::{ - RpcContextualPeerAddress, RpcError, RpcExtraData, RpcHash, RpcIpAddress, RpcNetworkType, RpcPeerAddress, RpcResult, - SubmitBlockRejectReason, SubmitBlockReport, + FeePerMass, RpcContextualPeerAddress, RpcError, RpcExtraData, RpcHash, RpcIpAddress, RpcNetworkType, RpcPeerAddress, RpcResult, + SubmitBlockRejectReason, SubmitBlockReport, VirtualFeePerMass, }; use std::str::FromStr; @@ -394,6 +394,29 @@ from!(item: RpcResult<&kaspa_rpc_core::GetDaaScoreTimestampEstimateResponse>, pr Self { timestamps: item.timestamps.clone(), error: None } }); +from!(_item: &kaspa_rpc_core::GetFeeInfoRequest, protowire::GetFeeInfoRequestMessage, { + Self {} +}); + +from!(item: RpcResult<&kaspa_rpc_core::GetFeeInfoResponse>, protowire::GetFeeInfoResponseMessage, { + let fee_per_mass = match item.fee_per_mass { + FeePerMass::VirtualFeePerMass(VirtualFeePerMass{max, median, min}) => { + protowire::get_fee_info_response_message::FeePerMass::Virtual( + protowire::Virtual{ + max, + median, + min, + } + ) + } + }; + Self { + fee_per_mass: Some(fee_per_mass), + mempool_total_mass: item.mempool_total_mass, + error: None, + } +}); + from!(&kaspa_rpc_core::PingRequest, protowire::PingRequestMessage); from!(RpcResult<&kaspa_rpc_core::PingResponse>, protowire::PingResponseMessage); @@ -791,6 +814,31 @@ try_from!(item: &protowire::GetDaaScoreTimestampEstimateResponseMessage, RpcResu Self { timestamps: item.timestamps.clone() } }); +try_from!(_item: &protowire::GetFeeInfoRequestMessage, kaspa_rpc_core::GetFeeInfoRequest , { + Self {} +}); + +try_from!(item: &protowire::GetFeeInfoResponseMessage, RpcResult, { + let fee_per_mass = item.fee_per_mass.as_ref().ok_or(RpcError::MissingRpcFieldError( + "GetFeeInfoResponseMessage".to_string(), + "fee_per_mass".to_string(), + ) + )?; + + match fee_per_mass { + protowire::get_fee_info_response_message::FeePerMass::Virtual(protowire::Virtual{max, median, min}) => { + kaspa_rpc_core::GetFeeInfoResponse{ + fee_per_mass: kaspa_rpc_core::FeePerMass::VirtualFeePerMass( + VirtualFeePerMass{ + max: *max , median: *median , min: *min, + } + ), + mempool_total_mass: item.mempool_total_mass, + } + }, + } +}); + try_from!(&protowire::PingRequestMessage, kaspa_rpc_core::PingRequest); try_from!(&protowire::PingResponseMessage, RpcResult); diff --git a/rpc/grpc/core/src/ops.rs b/rpc/grpc/core/src/ops.rs index 7cc23f1609..80be6124df 100644 --- a/rpc/grpc/core/src/ops.rs +++ b/rpc/grpc/core/src/ops.rs @@ -81,6 +81,7 @@ pub enum KaspadPayloadOps { GetServerInfo, GetSyncStatus, GetDaaScoreTimestampEstimate, + GetFeeInfo, // Subscription commands for starting/stopping notifications NotifyBlockAdded, diff --git a/rpc/grpc/server/src/request_handler/factory.rs b/rpc/grpc/server/src/request_handler/factory.rs index 802cb6cd6b..f9b604399d 100644 --- a/rpc/grpc/server/src/request_handler/factory.rs +++ b/rpc/grpc/server/src/request_handler/factory.rs @@ -75,6 +75,7 @@ impl Factory { GetServerInfo, GetSyncStatus, GetDaaScoreTimestampEstimate, + GetFeeInfo, NotifyBlockAdded, NotifyNewBlockTemplate, NotifyFinalityConflict, diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index ddf78ccbd7..b665200dc3 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -228,6 +228,10 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_fee_info_call(&self, _request: GetFeeInfoRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/service/Cargo.toml b/rpc/service/Cargo.toml index d606d51533..c93056eece 100644 --- a/rpc/service/Cargo.toml +++ b/rpc/service/Cargo.toml @@ -31,6 +31,7 @@ kaspa-utxoindex.workspace = true async-trait.workspace = true log.workspace = true +portable-atomic.workspace = true tokio.workspace = true triggered.workspace = true workflow-rpc.workspace = true diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index 00cc0d0828..202a526525 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -4,6 +4,7 @@ use super::collector::{CollectorFromConsensus, CollectorFromIndex}; use crate::converter::{consensus::ConsensusConverter, index::IndexConverter, protocol::ProtocolConverter}; use crate::service::NetworkType::{Mainnet, Testnet}; use async_trait::async_trait; +use kaspa_addresses::{Address, Version}; use kaspa_consensus_core::api::counters::ProcessingCounters; use kaspa_consensus_core::errors::block::RuleError; use kaspa_consensus_core::{ @@ -64,6 +65,8 @@ use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script}; use kaspa_utils::{channel::Channel, triggers::SingleTrigger}; use kaspa_utils_tower::counters::TowerConnectionCounters; use kaspa_utxoindex::api::UtxoIndexProxy; +use std::ops::Mul; +use std::sync::atomic::{AtomicBool, AtomicU64}; use std::{ collections::HashMap, iter::once, @@ -109,6 +112,8 @@ pub struct RpcCoreService { perf_monitor: Arc>>, p2p_tower_counters: Arc, grpc_tower_counters: Arc, + + estimated_fee_cache: EstimatedFeeCache, } const RPC_CORE: &str = "rpc-core"; @@ -208,6 +213,7 @@ impl RpcCoreService { perf_monitor, p2p_tower_counters, grpc_tower_counters, + estimated_fee_cache: Default::default(), } } @@ -646,6 +652,90 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetDaaScoreTimestampEstimateResponse::new(timestamps)) } + async fn get_fee_info_call(&self, _request: GetFeeInfoRequest) -> RpcResult { + trace!("incoming GetFeeInfoRequest request"); + const RECALC_TIMEOUT_IN_SEC: u64 = 1; + const EXPIRATION_IN_SEC: u64 = 2; + + let relaxed_cache_resp = || GetFeeInfoResponse { + fee_per_mass: FeePerMass::VirtualFeePerMass(VirtualFeePerMass { + max: self.estimated_fee_cache.max.load(Ordering::Relaxed), + median: self.estimated_fee_cache.median.load(Ordering::Relaxed), + min: self.estimated_fee_cache.min.load(Ordering::Relaxed), + }), + mempool_total_mass: self.mining_manager.snapshot().total_mass, + }; + let calculated_at = self.estimated_fee_cache.calculated_at.load(Ordering::Relaxed); + let now = unix_now(); + if calculated_at + RECALC_TIMEOUT_IN_SEC > now { + debug!("Cache is not expired, return response from cache"); + return Ok(relaxed_cache_resp()); + } + + let captured = self + .estimated_fee_cache + .computing_in_progress + .compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed) + .is_ok(); + let expired = calculated_at + EXPIRATION_IN_SEC < now; + if !captured && !expired { + return Ok(relaxed_cache_resp()); + } + + let compute = || async { + let script_public_key = + kaspa_txscript::pay_to_address_script(&Address::new(self.config.net.into(), Version::PubKey, &[0u8; 32])); + let miner_data: MinerData = MinerData::new(script_public_key, vec![]); + let session = self.consensus_manager.consensus().unguarded_session(); + let kaspa_consensus_core::block::BlockTemplate { + block: kaspa_consensus_core::block::MutableBlock { transactions, .. }, + calculated_fees, + .. + } = self.mining_manager.clone().get_block_template(&session, miner_data).await?; + // don't count coinbase tx + if transactions.len() < 2 { + debug!("Block template has no txs, return response from cache"); + return Ok(relaxed_cache_resp()); + } + let transactions = &transactions[1..]; // skip coinbase tx + let mut fees_and_masses = Vec::with_capacity(transactions.len()); + for (mass, fee) in transactions.iter().map(Transaction::mass).zip(calculated_fees) { + fees_and_masses.push((fee, mass)); + } + fees_and_masses + .sort_unstable_by(|(lhs_fee, lhs_mass), (rhs_fee, rhs_mass)| lhs_fee.mul(rhs_mass).cmp(&rhs_fee.mul(lhs_mass))); + let max = { + let (fee, mass) = fees_and_masses.last().unwrap(); + *fee as f64 / *mass as f64 + }; + let min = { + let (fee, mass) = fees_and_masses.first().unwrap(); + *fee as f64 / *mass as f64 + }; + let median = { + let (fee, mass) = fees_and_masses[fees_and_masses.len() / 2]; + fee as f64 / mass as f64 + }; + if captured { + self.estimated_fee_cache.max.store(max, Ordering::Relaxed); + self.estimated_fee_cache.min.store(min, Ordering::Relaxed); + self.estimated_fee_cache.median.store(median, Ordering::Relaxed); + self.estimated_fee_cache.calculated_at.store(unix_now(), Ordering::Relaxed); + } + + debug!("Computation is successful. Updating cache and returning fresh response"); + + Ok(GetFeeInfoResponse { + fee_per_mass: FeePerMass::VirtualFeePerMass(VirtualFeePerMass { max, median, min }), + mempool_total_mass: self.mining_manager.snapshot().total_mass, + }) + }; + let res = compute().await; + if captured { + self.estimated_fee_cache.computing_in_progress.store(false, Ordering::Release); + } + res + } async fn ping_call(&self, _: PingRequest) -> RpcResult { Ok(PingResponse {}) @@ -977,3 +1067,12 @@ impl AsyncService for RpcCoreService { }) } } + +#[derive(Debug, Default)] +struct EstimatedFeeCache { + min: portable_atomic::AtomicF64, + median: portable_atomic::AtomicF64, + max: portable_atomic::AtomicF64, + calculated_at: AtomicU64, + computing_in_progress: AtomicBool, +} diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index 4e9fffc0c5..122500f384 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -597,6 +597,7 @@ impl RpcApi for KaspaRpcClient { GetCoinSupply, GetConnectedPeerInfo, GetDaaScoreTimestampEstimate, + GetFeeInfo, GetServerInfo, GetCurrentNetwork, GetHeaders, diff --git a/rpc/wrpc/server/src/router.rs b/rpc/wrpc/server/src/router.rs index af46266811..2720f2c5c8 100644 --- a/rpc/wrpc/server/src/router.rs +++ b/rpc/wrpc/server/src/router.rs @@ -45,6 +45,7 @@ impl Router { GetCoinSupply, GetConnectedPeerInfo, GetDaaScoreTimestampEstimate, + GetFeeInfo, GetServerInfo, GetCurrentNetwork, GetHeaders, diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index 3224cefee9..54271679ea 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -621,6 +621,12 @@ async fn sanity_test() { rpc_client.stop_notify(id, PruningPointUtxoSetOverrideScope {}.into()).await.unwrap(); }) } + KaspadPayloadOps::GetFeeInfo => { + let rpc_client = client.clone(); + tst!(op, { + let _ = rpc_client.get_fee_info_call(GetFeeInfoRequest {}).await.unwrap(); + }) + } }; tasks.push(task); } diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 6c335d59a6..eb44ba6e27 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -245,6 +245,10 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_fee_info_call(&self, _request: GetFeeInfoRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/wasm/build/docs/typedoc.json b/wasm/build/docs/typedoc.json index b89af0882c..db308c812b 100644 --- a/wasm/build/docs/typedoc.json +++ b/wasm/build/docs/typedoc.json @@ -1,7 +1,7 @@ { "$schema": "https://typedoc.org/schema.json", - "treatWarningsAsErrors": true, + "treatWarningsAsErrors": false, "cleanOutputDir": true, "disableSources": true, - "categoryOrder": ["*", "Other"], + "categoryOrder": ["*", "Other"] }