diff --git a/Cargo.lock b/Cargo.lock index bd09682ce5..9b70aeba13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -891,6 +891,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ckb-fee-estimator" +version = "0.119.0-pre" +dependencies = [ + "ckb-chain-spec", + "ckb-logger", + "ckb-types", + "ckb-util", + "thiserror", +] + [[package]] name = "ckb-fixed-hash" version = "0.119.0-pre" @@ -1537,6 +1548,7 @@ dependencies = [ "ckb-db", "ckb-db-schema", "ckb-error", + "ckb-fee-estimator", "ckb-logger", "ckb-metrics", "ckb-migrate", @@ -1708,6 +1720,7 @@ dependencies = [ "ckb-dao", "ckb-db", "ckb-error", + "ckb-fee-estimator", "ckb-hash", "ckb-jsonrpc-types", "ckb-logger", diff --git a/Cargo.toml b/Cargo.toml index d0fd227a91..21c7230495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "util/dao/utils", "traits", "spec", + "util/fee-estimator", "util/proposal-table", "script", "util/app-config", diff --git a/chain/src/verify.rs b/chain/src/verify.rs index 1b2a007aa1..dbd1839b64 100644 --- a/chain/src/verify.rs +++ b/chain/src/verify.rs @@ -306,6 +306,8 @@ impl ConsumeUnverifiedBlockProcessor { db_txn.insert_epoch_ext(&epoch.last_block_hash_in_previous_epoch(), &epoch)?; } + let in_ibd = self.shared.is_initial_block_download(); + if new_best_block { info!( "[verify block] new best block found: {} => {:#x}, difficulty diff = {:#x}, unverified_tip: {}", @@ -368,6 +370,9 @@ impl ConsumeUnverifiedBlockProcessor { ) { error!("[verify block] notify update_tx_pool_for_reorg error {}", e); } + if let Err(e) = tx_pool_controller.update_ibd_state(in_ibd) { + error!("Notify update_ibd_state error {}", e); + } } self.shared @@ -395,6 +400,9 @@ impl ConsumeUnverifiedBlockProcessor { if let Err(e) = tx_pool_controller.notify_new_uncle(block.as_uncle()) { error!("[verify block] notify new_uncle error {}", e); } + if let Err(e) = tx_pool_controller.update_ibd_state(in_ibd) { + error!("Notify update_ibd_state error {}", e); + } } } Ok(true) diff --git a/resource/ckb.toml b/resource/ckb.toml index 262b5fa496..68cf8e2fd8 100644 --- a/resource/ckb.toml +++ b/resource/ckb.toml @@ -234,3 +234,7 @@ block_uncles_cache_size = 30 # db_port = 5432 # db_user = "postgres" # db_password = "123456" +# +# # [fee_estimator] +# # Specifies the fee estimates algorithm. Current algorithms: ConfirmationFraction, WeightUnitsFlow. +# # algorithm = "WeightUnitsFlow" diff --git a/rpc/README.md b/rpc/README.md index c94b79d98e..ac14673823 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -70,6 +70,7 @@ The crate `ckb-rpc`'s minimum supported rustc version is 1.71.1. * [Method `dry_run_transaction`](#experiment-dry_run_transaction) * [Method `calculate_dao_maximum_withdraw`](#experiment-calculate_dao_maximum_withdraw) + * [Method `estimate_fee_rate`](#experiment-estimate_fee_rate) * [Module Indexer](#module-indexer) [👉 OpenRPC spec](http://playground.open-rpc.org/?uiSchema[appBar][ui:title]=CKB-Indexer&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:examplesDropdown]=false&uiSchema[appBar][ui:logoUrl]=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/ckb-logo.jpg&schemaUrl=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/json/indexer_rpc_doc.json) * [Method `get_indexer_tip`](#indexer-get_indexer_tip) @@ -171,6 +172,7 @@ The crate `ckb-rpc`'s minimum supported rustc version is 1.71.1. * [Type `EpochNumberWithFraction`](#type-epochnumberwithfraction) * [Type `EpochView`](#type-epochview) * [Type `EstimateCycles`](#type-estimatecycles) + * [Type `EstimateMode`](#type-estimatemode) * [Type `ExtraLoggerConfig`](#type-extraloggerconfig) * [Type `FeeRateStatistics`](#type-feeratestatistics) * [Type `H256`](#type-h256) @@ -2168,6 +2170,62 @@ Response } ``` + +#### Method `estimate_fee_rate` +* `estimate_fee_rate(estimate_mode, enable_fallback)` + * `estimate_mode`: [`EstimateMode`](#type-estimatemode) `|` `null` + * `enable_fallback`: `boolean` `|` `null` +* result: [`Uint64`](#type-uint64) + +Get fee estimates. + +###### Params + +* `estimate_mode` - The fee estimate mode. + + Default: `no_priority`. + +* `enable_fallback` - True to enable a simple fallback algorithm, when lack of historical empirical data to estimate fee rates with configured algorithm. + + Default: `true`. + +####### The fallback algorithm + +Since CKB transaction confirmation involves a two-step process—1) propose and 2) commit, it is complex to +predict the transaction fee accurately with the expectation that it will be included within a certain block height. + +This algorithm relies on two assumptions and uses a simple strategy to estimate the transaction fee: 1) all transactions +in the pool are waiting to be proposed, and 2) no new transactions will be added to the pool. + +In practice, this simple algorithm should achieve good accuracy fee rate and running performance. + +###### Returns + +The estimated fee rate in shannons per kilobyte. + +###### Examples + +Request + +```json +{ + "id": 42, + "jsonrpc": "2.0", + "method": "estimate_fee_rate", + "params": [] +} +``` + +Response + +```json +{ + "id": 42, + "jsonrpc": "2.0", + "result": "0x3e8" +} +``` + ### Module `Indexer` - [👉 OpenRPC spec](http://playground.open-rpc.org/?uiSchema[appBar][ui:title]=CKB-Indexer&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:examplesDropdown]=false&uiSchema[appBar][ui:logoUrl]=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/ckb-logo.jpg&schemaUrl=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/json/indexer_rpc_doc.json) @@ -6064,6 +6122,15 @@ Response result of the RPC method `estimate_cycles`. * `cycles`: [`Uint64`](#type-uint64) - The count of cycles that the VM has consumed to verify this transaction. +### Type `EstimateMode` +The fee estimate mode. + +It's an enum value from one of: + - no_priority : No priority, expect the transaction to be committed in 1 hour. + - low_priority : Low priority, expect the transaction to be committed in 30 minutes. + - medium_priority : Medium priority, expect the transaction to be committed in 10 minutes. + - high_priority : High priority, expect the transaction to be committed as soon as possible. + ### Type `ExtraLoggerConfig` Runtime logger config for extra loggers. diff --git a/rpc/src/module/experiment.rs b/rpc/src/module/experiment.rs index 90e910dc1a..ba4e93b065 100644 --- a/rpc/src/module/experiment.rs +++ b/rpc/src/module/experiment.rs @@ -3,7 +3,8 @@ use crate::module::chain::CyclesEstimator; use async_trait::async_trait; use ckb_dao::DaoCalculator; use ckb_jsonrpc_types::{ - Capacity, DaoWithdrawingCalculationKind, EstimateCycles, OutPoint, Transaction, + Capacity, DaoWithdrawingCalculationKind, EstimateCycles, EstimateMode, OutPoint, Transaction, + Uint64, }; use ckb_shared::{shared::Shared, Snapshot}; use ckb_store::ChainStore; @@ -162,6 +163,61 @@ pub trait ExperimentRpc { out_point: OutPoint, kind: DaoWithdrawingCalculationKind, ) -> Result; + + /// Get fee estimates. + /// + /// ## Params + /// + /// * `estimate_mode` - The fee estimate mode. + /// + /// Default: `no_priority`. + /// + /// * `enable_fallback` - True to enable a simple fallback algorithm, when lack of historical empirical data to estimate fee rates with configured algorithm. + /// + /// Default: `true`. + /// + /// ### The fallback algorithm + /// + /// Since CKB transaction confirmation involves a two-step process—1) propose and 2) commit, it is complex to + /// predict the transaction fee accurately with the expectation that it will be included within a certain block height. + /// + /// This algorithm relies on two assumptions and uses a simple strategy to estimate the transaction fee: 1) all transactions + /// in the pool are waiting to be proposed, and 2) no new transactions will be added to the pool. + /// + /// In practice, this simple algorithm should achieve good accuracy fee rate and running performance. + /// + /// ## Returns + /// + /// The estimated fee rate in shannons per kilobyte. + /// + /// ## Examples + /// + /// Request + /// + /// ```json + /// { + /// "id": 42, + /// "jsonrpc": "2.0", + /// "method": "estimate_fee_rate", + /// "params": [] + /// } + /// ``` + /// + /// Response + /// + /// ```json + /// { + /// "id": 42, + /// "jsonrpc": "2.0", + /// "result": "0x3e8" + /// } + /// ``` + #[rpc(name = "estimate_fee_rate")] + fn estimate_fee_rate( + &self, + estimate_mode: Option, + enable_fallback: Option, + ) -> Result; } #[derive(Clone)] @@ -241,4 +297,20 @@ impl ExperimentRpc for ExperimentRpcImpl { } } } + + fn estimate_fee_rate( + &self, + estimate_mode: Option, + enable_fallback: Option, + ) -> Result { + let estimate_mode = estimate_mode.unwrap_or_default(); + let enable_fallback = enable_fallback.unwrap_or(true); + self.shared + .tx_pool_controller() + .estimate_fee_rate(estimate_mode.into(), enable_fallback) + .map_err(|err| RPCError::custom(RPCError::CKBInternalError, err.to_string()))? + .map_err(RPCError::from_any_error) + .map(core::FeeRate::as_u64) + .map(Into::into) + } } diff --git a/rpc/src/tests/examples.rs b/rpc/src/tests/examples.rs index 03afbb9d3c..20b87f7ae9 100644 --- a/rpc/src/tests/examples.rs +++ b/rpc/src/tests/examples.rs @@ -389,6 +389,7 @@ fn mock_rpc_response(example: &RpcTestExample, response: &mut RpcTestResponse) { "get_pool_tx_detail_info" => { response.result["timestamp"] = example.response.result["timestamp"].clone() } + "estimate_fee_rate" => replace_rpc_response::(example, response), _ => {} } } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 58f96716c8..affce7214d 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -29,6 +29,7 @@ ckb-systemtime = { path = "../util/systemtime", version = "= 0.119.0-pre" } ckb-channel = { path = "../util/channel", version = "= 0.119.0-pre" } ckb-app-config = { path = "../util/app-config", version = "= 0.119.0-pre" } ckb-migrate = { path = "../util/migrate", version = "= 0.119.0-pre" } +ckb-fee-estimator = {path = "../util/fee-estimator", version = "= 0.119.0-pre"} once_cell = "1.8.0" ckb-util = { path = "../util", version = "= 0.119.0-pre" } ckb-metrics = { path = "../util/metrics", version = "= 0.119.0-pre" } diff --git a/shared/src/shared_builder.rs b/shared/src/shared_builder.rs index 444ef4fe9e..9d885a6f70 100644 --- a/shared/src/shared_builder.rs +++ b/shared/src/shared_builder.rs @@ -2,7 +2,8 @@ use crate::ChainServicesBuilder; use crate::{HeaderMap, Shared}; use ckb_app_config::{ - BlockAssemblerConfig, DBConfig, ExitCode, NotifyConfig, StoreConfig, SyncConfig, TxPoolConfig, + BlockAssemblerConfig, DBConfig, ExitCode, FeeEstimatorAlgo, FeeEstimatorConfig, NotifyConfig, + StoreConfig, SyncConfig, TxPoolConfig, }; use ckb_async_runtime::{new_background_runtime, Handle}; use ckb_chain_spec::consensus::Consensus; @@ -11,6 +12,7 @@ use ckb_channel::Receiver; use ckb_db::RocksDB; use ckb_db_schema::COLUMNS; use ckb_error::{Error, InternalErrorKind}; +use ckb_fee_estimator::FeeEstimator; use ckb_logger::{error, info}; use ckb_migrate::migrate::Migrate; use ckb_notify::{NotifyController, NotifyService}; @@ -47,6 +49,7 @@ pub struct SharedBuilder { block_assembler_config: Option, notify_config: Option, async_handle: Handle, + fee_estimator_config: Option, header_map_tmp_dir: Option, } @@ -153,6 +156,7 @@ impl SharedBuilder { sync_config: None, block_assembler_config: None, async_handle, + fee_estimator_config: None, header_map_tmp_dir: None, }) } @@ -200,6 +204,7 @@ impl SharedBuilder { sync_config: None, block_assembler_config: None, async_handle: runtime.get_or_init(new_background_runtime).clone(), + fee_estimator_config: None, header_map_tmp_dir: None, }) @@ -249,6 +254,12 @@ impl SharedBuilder { self } + /// Sets the configuration for the fee estimator. + pub fn fee_estimator_config(mut self, config: FeeEstimatorConfig) -> Self { + self.fee_estimator_config = Some(config); + self + } + /// specifies the async_handle for the shared pub fn async_handle(mut self, async_handle: Handle) -> Self { self.async_handle = async_handle; @@ -364,6 +375,7 @@ impl SharedBuilder { block_assembler_config, notify_config, async_handle, + fee_estimator_config, header_map_tmp_dir, } = self; @@ -403,6 +415,17 @@ impl SharedBuilder { let (sender, receiver) = ckb_channel::unbounded(); + let fee_estimator_algo = fee_estimator_config + .map(|config| config.algorithm) + .unwrap_or(None); + let fee_estimator = match fee_estimator_algo { + Some(FeeEstimatorAlgo::WeightUnitsFlow) => FeeEstimator::new_weight_units_flow(), + Some(FeeEstimatorAlgo::ConfirmationFraction) => { + FeeEstimator::new_confirmation_fraction() + } + None => FeeEstimator::new_dummy(), + }; + let (mut tx_pool_builder, tx_pool_controller) = TxPoolServiceBuilder::new( tx_pool_config, Arc::clone(&snapshot), @@ -410,9 +433,14 @@ impl SharedBuilder { Arc::clone(&txs_verify_cache), &async_handle, sender, + fee_estimator.clone(), ); - register_tx_pool_callback(&mut tx_pool_builder, notify_controller.clone()); + register_tx_pool_callback( + &mut tx_pool_builder, + notify_controller.clone(), + fee_estimator, + ); let block_status_map = Arc::new(DashMap::new()); @@ -499,7 +527,11 @@ fn build_store( Ok(store) } -fn register_tx_pool_callback(tx_pool_builder: &mut TxPoolServiceBuilder, notify: NotifyController) { +fn register_tx_pool_callback( + tx_pool_builder: &mut TxPoolServiceBuilder, + notify: NotifyController, + fee_estimator: FeeEstimator, +) { let notify_pending = notify.clone(); let tx_relay_sender = tx_pool_builder.tx_relay_sender(); @@ -510,10 +542,15 @@ fn register_tx_pool_callback(tx_pool_builder: &mut TxPoolServiceBuilder, notify: fee: entry.fee, timestamp: entry.timestamp, }; + + let fee_estimator_clone = fee_estimator.clone(); tx_pool_builder.register_pending(Box::new(move |entry: &TxEntry| { // notify let notify_tx_entry = create_notify_entry(entry); notify_pending.notify_new_transaction(notify_tx_entry); + let tx_hash = entry.transaction().hash(); + let entry_info = entry.to_info(); + fee_estimator_clone.accept_tx(tx_hash, entry_info); })); let notify_proposed = notify.clone(); @@ -537,7 +574,9 @@ fn register_tx_pool_callback(tx_pool_builder: &mut TxPoolServiceBuilder, notify: } if reject.is_allowed_relay() { - if let Err(e) = tx_relay_sender.send(TxVerificationResult::Reject { tx_hash }) { + if let Err(e) = tx_relay_sender.send(TxVerificationResult::Reject { + tx_hash: tx_hash.clone(), + }) { error!("tx-pool tx_relay_sender internal error {}", e); } } @@ -545,6 +584,9 @@ fn register_tx_pool_callback(tx_pool_builder: &mut TxPoolServiceBuilder, notify: // notify let notify_tx_entry = create_notify_entry(entry); notify_reject.notify_reject_transaction(notify_tx_entry, reject); + + // fee estimator + fee_estimator.reject_tx(&tx_hash); }, )); } diff --git a/spec/src/consensus.rs b/spec/src/consensus.rs index 2d8a16c481..3d312a9a8b 100644 --- a/spec/src/consensus.rs +++ b/spec/src/consensus.rs @@ -45,7 +45,8 @@ pub(crate) const DEFAULT_SECONDARY_EPOCH_REWARD: Capacity = Capacity::shannons(6 // 4.2 billion per year pub(crate) const INITIAL_PRIMARY_EPOCH_REWARD: Capacity = Capacity::shannons(1_917_808_21917808); const MAX_UNCLE_NUM: usize = 2; -pub(crate) const TX_PROPOSAL_WINDOW: ProposalWindow = ProposalWindow(2, 10); +/// Default transaction proposal window. +pub const TX_PROPOSAL_WINDOW: ProposalWindow = ProposalWindow(2, 10); // Cellbase outputs are "locked" and require 4 epoch confirmations (approximately 16 hours) before // they mature sufficiently to be spendable, // This is to reduce the risk of later txs being reversed if a chain reorganization occurs. @@ -138,17 +139,17 @@ pub const TYPE_ID_CODE_HASH: H256 = h256!("0x545950455f4944"); /// impl ProposalWindow { /// The w_close parameter - pub fn closest(&self) -> BlockNumber { + pub const fn closest(&self) -> BlockNumber { self.0 } /// The w_far parameter - pub fn farthest(&self) -> BlockNumber { + pub const fn farthest(&self) -> BlockNumber { self.1 } /// The proposal window length - pub fn length(&self) -> BlockNumber { + pub const fn length(&self) -> BlockNumber { self.1 - self.0 + 1 } } diff --git a/tx-pool/Cargo.toml b/tx-pool/Cargo.toml index 79e459a362..20ba77cc70 100644 --- a/tx-pool/Cargo.toml +++ b/tx-pool/Cargo.toml @@ -44,6 +44,7 @@ multi_index_map = "0.6.0" slab = "0.4" rustc-hash = "1.1" tokio-util = "0.7.8" +ckb-fee-estimator = { path = "../util/fee-estimator", version = "= 0.119.0-pre" } [dev-dependencies] tempfile.workspace = true diff --git a/tx-pool/src/component/pool_map.rs b/tx-pool/src/component/pool_map.rs index e846dbfbc0..dc54a89bc3 100644 --- a/tx-pool/src/component/pool_map.rs +++ b/tx-pool/src/component/pool_map.rs @@ -9,7 +9,7 @@ use crate::error::Reject; use crate::TxEntry; use ckb_logger::{debug, error, trace}; use ckb_types::core::error::OutPointError; -use ckb_types::core::Cycle; +use ckb_types::core::{Cycle, FeeRate}; use ckb_types::packed::OutPoint; use ckb_types::prelude::*; use ckb_types::{ @@ -329,6 +329,33 @@ impl PoolMap { conflicts } + pub(crate) fn estimate_fee_rate( + &self, + mut target_blocks: usize, + max_block_bytes: usize, + max_block_cycles: Cycle, + min_fee_rate: FeeRate, + ) -> FeeRate { + debug_assert!(target_blocks > 0); + let iter = self.entries.iter_by_score().rev(); + let mut current_block_bytes = 0; + let mut current_block_cycles = 0; + for entry in iter { + current_block_bytes += entry.inner.size; + current_block_cycles += entry.inner.cycles; + if current_block_bytes >= max_block_bytes || current_block_cycles >= max_block_cycles { + target_blocks -= 1; + if target_blocks == 0 { + return entry.inner.fee_rate(); + } + current_block_bytes = entry.inner.size; + current_block_cycles = entry.inner.cycles; + } + } + + min_fee_rate + } + // find the pending txs sorted by score, and return their proposal short ids pub(crate) fn get_proposals( &self, diff --git a/tx-pool/src/component/tests/estimate.rs b/tx-pool/src/component/tests/estimate.rs new file mode 100644 index 0000000000..fa222fd91a --- /dev/null +++ b/tx-pool/src/component/tests/estimate.rs @@ -0,0 +1,56 @@ +use crate::component::tests::util::build_tx; +use crate::component::{ + entry::TxEntry, + pool_map::{PoolMap, Status}, +}; +use ckb_types::core::{Capacity, Cycle, FeeRate}; + +#[test] +fn test_estimate_fee_rate() { + let mut pool = PoolMap::new(1000); + for i in 0..1024 { + let tx = build_tx(vec![(&Default::default(), i as u32)], 1); + let entry = TxEntry::dummy_resolve(tx, i + 1, Capacity::shannons(i + 1), 1000); + pool.add_entry(entry, Status::Pending).unwrap(); + } + + assert_eq!( + FeeRate::from_u64(42), + pool.estimate_fee_rate(1, usize::MAX, Cycle::MAX, FeeRate::from_u64(42)) + ); + + assert_eq!( + FeeRate::from_u64(1024), + pool.estimate_fee_rate(1, 1000, Cycle::MAX, FeeRate::from_u64(1)) + ); + assert_eq!( + FeeRate::from_u64(1023), + pool.estimate_fee_rate(1, 2000, Cycle::MAX, FeeRate::from_u64(1)) + ); + assert_eq!( + FeeRate::from_u64(1016), + pool.estimate_fee_rate(2, 5000, Cycle::MAX, FeeRate::from_u64(1)) + ); + + assert_eq!( + FeeRate::from_u64(1024), + pool.estimate_fee_rate(1, usize::MAX, 1, FeeRate::from_u64(1)) + ); + assert_eq!( + FeeRate::from_u64(1023), + pool.estimate_fee_rate(1, usize::MAX, 2047, FeeRate::from_u64(1)) + ); + assert_eq!( + FeeRate::from_u64(1015), + pool.estimate_fee_rate(2, usize::MAX, 5110, FeeRate::from_u64(1)) + ); + + assert_eq!( + FeeRate::from_u64(624), + pool.estimate_fee_rate(100, 5000, 5110, FeeRate::from_u64(1)) + ); + assert_eq!( + FeeRate::from_u64(1), + pool.estimate_fee_rate(1000, 5000, 5110, FeeRate::from_u64(1)) + ); +} diff --git a/tx-pool/src/component/tests/mod.rs b/tx-pool/src/component/tests/mod.rs index ac625eede3..4f344ce443 100644 --- a/tx-pool/src/component/tests/mod.rs +++ b/tx-pool/src/component/tests/mod.rs @@ -1,5 +1,6 @@ mod chunk; mod entry; +mod estimate; mod links; mod orphan; mod pending; diff --git a/tx-pool/src/pool.rs b/tx-pool/src/pool.rs index da259d413c..66cbf0d424 100644 --- a/tx-pool/src/pool.rs +++ b/tx-pool/src/pool.rs @@ -8,11 +8,12 @@ use crate::component::recent_reject::RecentReject; use crate::error::Reject; use crate::pool_cell::PoolCell; use ckb_app_config::TxPoolConfig; +use ckb_fee_estimator::Error as FeeEstimatorError; use ckb_logger::{debug, error, warn}; use ckb_snapshot::Snapshot; use ckb_store::ChainStore; use ckb_types::core::tx_pool::PoolTxDetailInfo; -use ckb_types::core::CapacityError; +use ckb_types::core::{BlockNumber, CapacityError, FeeRate}; use ckb_types::packed::OutPoint; use ckb_types::{ core::{ @@ -553,6 +554,23 @@ impl TxPool { (entries, size, cycles) } + pub(crate) fn estimate_fee_rate( + &self, + target_to_be_committed: BlockNumber, + ) -> Result { + if !(3..=131).contains(&target_to_be_committed) { + return Err(FeeEstimatorError::NoProperFeeRate); + } + let fee_rate = self.pool_map.estimate_fee_rate( + (target_to_be_committed - self.snapshot.consensus().tx_proposal_window().closest()) + as usize, + self.snapshot.consensus().max_block_bytes() as usize, + self.snapshot.consensus().max_block_cycles(), + self.config.min_fee_rate, + ); + Ok(fee_rate) + } + pub(crate) fn check_rbf( &self, snapshot: &Snapshot, diff --git a/tx-pool/src/process.rs b/tx-pool/src/process.rs index 2594901784..8d38017ac1 100644 --- a/tx-pool/src/process.rs +++ b/tx-pool/src/process.rs @@ -12,6 +12,7 @@ use crate::util::{ }; use ckb_chain_spec::consensus::MAX_BLOCK_PROPOSALS_LIMIT; use ckb_error::{AnyError, InternalErrorKind}; +use ckb_fee_estimator::FeeEstimator; use ckb_jsonrpc_types::BlockTemplate; use ckb_logger::Level::Trace; use ckb_logger::{debug, error, info, log_enabled_target, trace_target}; @@ -20,7 +21,10 @@ use ckb_script::ChunkCommand; use ckb_snapshot::Snapshot; use ckb_types::core::error::OutPointError; use ckb_types::{ - core::{cell::ResolvedTransaction, BlockView, Capacity, Cycle, HeaderView, TransactionView}, + core::{ + cell::ResolvedTransaction, BlockView, Capacity, Cycle, EstimateMode, FeeRate, HeaderView, + TransactionView, + }, packed::{Byte32, ProposalShortId}, }; use ckb_util::LinkedHashSet; @@ -822,6 +826,7 @@ impl TxPoolService { } for blk in attached_blocks { + self.fee_estimator.commit_block(&blk); attached.extend(blk.transactions().into_iter().skip(1)); } let retain: Vec = detached.difference(&attached).cloned().collect(); @@ -1000,6 +1005,37 @@ impl TxPoolService { } } + pub(crate) async fn update_ibd_state(&self, in_ibd: bool) { + self.fee_estimator.update_ibd_state(in_ibd); + } + + pub(crate) async fn estimate_fee_rate( + &self, + estimate_mode: EstimateMode, + enable_fallback: bool, + ) -> Result { + let all_entry_info = self.tx_pool.read().await.get_all_entry_info(); + match self + .fee_estimator + .estimate_fee_rate(estimate_mode, all_entry_info) + { + Ok(fee_rate) => Ok(fee_rate), + Err(err) => { + if enable_fallback { + let target_blocks = + FeeEstimator::target_blocks_for_estimate_mode(estimate_mode); + self.tx_pool + .read() + .await + .estimate_fee_rate(target_blocks) + .map_err(Into::into) + } else { + Err(err.into()) + } + } + } + } + // # Notice // // This method assumes that the inputs transactions are sorted. diff --git a/tx-pool/src/service.rs b/tx-pool/src/service.rs index 6645550af8..7ed781254e 100644 --- a/tx-pool/src/service.rs +++ b/tx-pool/src/service.rs @@ -14,6 +14,7 @@ use ckb_async_runtime::Handle; use ckb_chain_spec::consensus::Consensus; use ckb_channel::oneshot; use ckb_error::AnyError; +use ckb_fee_estimator::FeeEstimator; use ckb_jsonrpc_types::BlockTemplate; use ckb_logger::error; use ckb_logger::info; @@ -22,15 +23,16 @@ use ckb_script::ChunkCommand; use ckb_snapshot::Snapshot; use ckb_stop_handler::new_tokio_exit_rx; use ckb_store::ChainStore; -use ckb_types::core::cell::{CellProvider, CellStatus, OverlayCellProvider}; -use ckb_types::core::tx_pool::{EntryCompleted, PoolTxDetailInfo, TransactionWithStatus, TxStatus}; -use ckb_types::packed::OutPoint; use ckb_types::{ core::{ - tx_pool::{Reject, TxPoolEntryInfo, TxPoolIds, TxPoolInfo, TRANSACTION_SIZE_LIMIT}, - BlockView, Cycle, TransactionView, UncleBlockView, Version, + cell::{CellProvider, CellStatus, OverlayCellProvider}, + tx_pool::{ + EntryCompleted, PoolTxDetailInfo, Reject, TransactionWithStatus, TxPoolEntryInfo, + TxPoolIds, TxPoolInfo, TxStatus, TRANSACTION_SIZE_LIMIT, + }, + BlockView, Cycle, EstimateMode, FeeRate, TransactionView, UncleBlockView, Version, }, - packed::{Byte32, ProposalShortId}, + packed::{Byte32, OutPoint, ProposalShortId}, }; use ckb_util::{LinkedHashMap, LinkedHashSet}; use ckb_verification::cache::TxVerificationCache; @@ -94,6 +96,8 @@ pub(crate) type ChainReorgArgs = ( Arc, ); +pub(crate) type FeeEstimatesResult = Result; + pub(crate) enum Message { BlockTemplate(Request), SubmitLocalTx(Request), @@ -116,6 +120,9 @@ pub(crate) enum Message { SavePool(Request<(), ()>), GetPoolTxDetails(Request), + UpdateIBDState(Request), + EstimateFeeRate(Request<(EstimateMode, bool), FeeEstimatesResult>), + // test #[cfg(feature = "internal")] PlugEntry(Request<(Vec, PlugTarget), ()>), @@ -349,6 +356,20 @@ impl TxPoolController { send_message!(self, SavePool, ()) } + /// Updates IBD state. + pub fn update_ibd_state(&self, in_ibd: bool) -> Result<(), AnyError> { + send_message!(self, UpdateIBDState, in_ibd) + } + + /// Estimates fee rate. + pub fn estimate_fee_rate( + &self, + estimate_mode: EstimateMode, + enable_fallback: bool, + ) -> Result { + send_message!(self, EstimateFeeRate, (estimate_mode, enable_fallback)) + } + /// Sends suspend chunk process cmd pub fn suspend_chunk_process(&self) -> Result<(), AnyError> { //debug!("[verify-test] run suspend_chunk_process"); @@ -426,6 +447,7 @@ pub struct TxPoolServiceBuilder { mpsc::Sender, mpsc::Receiver, ), + pub(crate) fee_estimator: FeeEstimator, } impl TxPoolServiceBuilder { @@ -437,6 +459,7 @@ impl TxPoolServiceBuilder { txs_verify_cache: Arc>, handle: &Handle, tx_relay_sender: ckb_channel::Sender, + fee_estimator: FeeEstimator, ) -> (TxPoolServiceBuilder, TxPoolController) { let (sender, receiver) = mpsc::channel(DEFAULT_CHANNEL_SIZE); let block_assembler_channel = mpsc::channel(BLOCK_ASSEMBLER_CHANNEL_SIZE); @@ -470,6 +493,7 @@ impl TxPoolServiceBuilder { chunk_rx, started, block_assembler_channel, + fee_estimator, }; (builder, controller) @@ -529,6 +553,7 @@ impl TxPoolServiceBuilder { consensus, delay: Arc::new(RwLock::new(LinkedHashMap::new())), after_delay: Arc::new(AtomicBool::new(after_delay_window)), + fee_estimator: self.fee_estimator, }; let mut verify_mgr = @@ -680,6 +705,7 @@ pub(crate) struct TxPoolService { pub(crate) block_assembler_sender: mpsc::Sender, pub(crate) delay: Arc>>, pub(crate) after_delay: Arc, + pub(crate) fee_estimator: FeeEstimator, } /// tx verification result @@ -959,6 +985,26 @@ async fn process(mut service: TxPoolService, message: Message) { error!("Responder sending save_pool failed {:?}", e) }; } + Message::UpdateIBDState(Request { + responder, + arguments: in_ibd, + }) => { + service.update_ibd_state(in_ibd).await; + if let Err(e) = responder.send(()) { + error!("Responder sending update_ibd_state failed {:?}", e) + }; + } + Message::EstimateFeeRate(Request { + responder, + arguments: (estimate_mode, enable_fallback), + }) => { + let fee_estimates_result = service + .estimate_fee_rate(estimate_mode, enable_fallback) + .await; + if let Err(e) = responder.send(fee_estimates_result) { + error!("Responder sending fee_estimates_result failed {:?}", e) + }; + } #[cfg(feature = "internal")] Message::PlugEntry(Request { responder, diff --git a/util/app-config/src/app_config.rs b/util/app-config/src/app_config.rs index 963bd7ef18..d10819c876 100644 --- a/util/app-config/src/app_config.rs +++ b/util/app-config/src/app_config.rs @@ -92,6 +92,9 @@ pub struct CKBAppConfig { /// Indexer config options. #[serde(default)] pub indexer: IndexerConfig, + /// Fee estimator config options. + #[serde(default)] + pub fee_estimator: FeeEstimatorConfig, } /// The miner config file for `ckb miner`. Usually it is the `ckb-miner.toml` in the CKB root diff --git a/util/app-config/src/configs/fee_estimator.rs b/util/app-config/src/configs/fee_estimator.rs new file mode 100644 index 0000000000..5ca05d46d8 --- /dev/null +++ b/util/app-config/src/configs/fee_estimator.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +/// Fee estimator config options. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// The algorithm for fee estimator. + pub algorithm: Option, +} + +/// Specifies the fee estimates algorithm. +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Eq)] +pub enum Algorithm { + /// Confirmation Fraction Fee Estimator + ConfirmationFraction, + /// Weight-Units Flow Fee Estimator + WeightUnitsFlow, +} diff --git a/util/app-config/src/configs/mod.rs b/util/app-config/src/configs/mod.rs index 7bcf193128..2dcfd82412 100644 --- a/util/app-config/src/configs/mod.rs +++ b/util/app-config/src/configs/mod.rs @@ -1,4 +1,5 @@ mod db; +mod fee_estimator; mod indexer; mod memory_tracker; mod miner; @@ -11,6 +12,7 @@ mod store; mod tx_pool; pub use db::Config as DBConfig; +pub use fee_estimator::{Algorithm as FeeEstimatorAlgo, Config as FeeEstimatorConfig}; pub use indexer::{IndexerConfig, IndexerSyncConfig}; pub use memory_tracker::Config as MemoryTrackerConfig; pub use miner::{ diff --git a/util/app-config/src/legacy/mod.rs b/util/app-config/src/legacy/mod.rs index 79d8c29b27..77084810d3 100644 --- a/util/app-config/src/legacy/mod.rs +++ b/util/app-config/src/legacy/mod.rs @@ -59,6 +59,8 @@ pub(crate) struct CKBAppConfig { notify: crate::NotifyConfig, #[serde(default)] indexer_v2: crate::IndexerConfig, + #[serde(default)] + fee_estimator: crate::FeeEstimatorConfig, } #[derive(Clone, Debug, Deserialize)] @@ -106,6 +108,7 @@ impl From for crate::CKBAppConfig { alert_signature, notify, indexer_v2, + fee_estimator, } = input; #[cfg(not(feature = "with_sentry"))] let _ = sentry; @@ -131,6 +134,7 @@ impl From for crate::CKBAppConfig { alert_signature, notify, indexer: indexer_v2, + fee_estimator, } } } diff --git a/util/fee-estimator/Cargo.toml b/util/fee-estimator/Cargo.toml new file mode 100644 index 0000000000..aaf0b0f227 --- /dev/null +++ b/util/fee-estimator/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ckb-fee-estimator" +version = "0.119.0-pre" +license = "MIT" +authors = ["Nervos Core Dev "] +edition = "2021" +description = "The ckb fee estimator" +homepage = "https://github.com/nervosnetwork/ckb" +repository = "https://github.com/nervosnetwork/ckb" + +[dependencies] +ckb-logger = { path = "../logger", version = "= 0.119.0-pre" } +ckb-types = { path = "../types", version = "= 0.119.0-pre" } +ckb-util = { path = "../../util", version = "= 0.119.0-pre" } +ckb-chain-spec = { path = "../../spec", version = "= 0.119.0-pre" } +thiserror = "1.0" diff --git a/util/fee-estimator/src/constants.rs b/util/fee-estimator/src/constants.rs new file mode 100644 index 0000000000..ca08d9fa0e --- /dev/null +++ b/util/fee-estimator/src/constants.rs @@ -0,0 +1,25 @@ +//! The constants for the fee estimator. + +use ckb_chain_spec::consensus::{MAX_BLOCK_INTERVAL, MIN_BLOCK_INTERVAL, TX_PROPOSAL_WINDOW}; +use ckb_types::core::{BlockNumber, FeeRate}; + +/// Average block interval (28). +pub(crate) const AVG_BLOCK_INTERVAL: u64 = (MAX_BLOCK_INTERVAL + MIN_BLOCK_INTERVAL) / 2; + +/// Max target blocks, about 1 hour (128). +pub(crate) const MAX_TARGET: BlockNumber = (60 * 60) / AVG_BLOCK_INTERVAL; +/// Min target blocks, in next block (5). +/// NOTE After tests, 3 blocks are too strict; so to adjust larger: 5. +pub(crate) const MIN_TARGET: BlockNumber = (TX_PROPOSAL_WINDOW.closest() + 1) + 2; + +/// Lowest fee rate. +pub(crate) const LOWEST_FEE_RATE: FeeRate = FeeRate::from_u64(1000); + +/// Target blocks for no priority (lowest priority, about 1 hour, 128). +pub const DEFAULT_TARGET: BlockNumber = MAX_TARGET; +/// Target blocks for low priority (about 30 minutes, 64). +pub const LOW_TARGET: BlockNumber = DEFAULT_TARGET / 2; +/// Target blocks for medium priority (about 10 minutes, 42). +pub const MEDIUM_TARGET: BlockNumber = LOW_TARGET / 3; +/// Target blocks for high priority (3). +pub const HIGH_TARGET: BlockNumber = MIN_TARGET; diff --git a/util/fee-estimator/src/error.rs b/util/fee-estimator/src/error.rs new file mode 100644 index 0000000000..f0798afc2d --- /dev/null +++ b/util/fee-estimator/src/error.rs @@ -0,0 +1,20 @@ +//! The error type for the fee estimator. + +use thiserror::Error; + +/// A list specifying general categories of fee estimator errors. +#[derive(Error, Debug, PartialEq)] +pub enum Error { + /// Dummy fee estimator is used. + #[error("dummy fee estimator is used")] + Dummy, + /// Not ready for do estimate. + #[error("not ready")] + NotReady, + /// Lack of empirical data. + #[error("lack of empirical data")] + LackData, + /// No proper fee rate. + #[error("no proper fee rate")] + NoProperFeeRate, +} diff --git a/util/fee-estimator/src/estimator/confirmation_fraction.rs b/util/fee-estimator/src/estimator/confirmation_fraction.rs new file mode 100644 index 0000000000..cc21d07c7d --- /dev/null +++ b/util/fee-estimator/src/estimator/confirmation_fraction.rs @@ -0,0 +1,556 @@ +//! Confirmation Fraction Fee Estimator +//! +//! Copy from https://github.com/nervosnetwork/ckb/tree/v0.39.1/util/fee-estimator +//! Ref: https://github.com/nervosnetwork/ckb/pull/1659 + +use std::{ + cmp, + collections::{BTreeMap, HashMap}, +}; + +use ckb_types::{ + core::{ + tx_pool::{get_transaction_weight, TxEntryInfo}, + BlockNumber, BlockView, FeeRate, + }, + packed::Byte32, +}; + +use crate::{constants, Error}; + +/// The number of blocks that the esitmator will trace the statistics. +const MAX_CONFIRM_BLOCKS: usize = 1000; +const DEFAULT_MIN_SAMPLES: usize = 20; +const DEFAULT_MIN_CONFIRM_RATE: f64 = 0.85; + +#[derive(Default, Debug, Clone)] +struct BucketStat { + total_fee_rate: FeeRate, + txs_count: f64, + old_unconfirmed_txs: usize, +} + +/// TxConfirmStat is a struct to help to estimate txs fee rate, +/// This struct record txs fee_rate and blocks that txs to be committed. +/// +/// We start from track unconfirmed txs, +/// When tx added to txpool, we increase the count of unconfirmed tx, we do opposite tx removed. +/// When a tx get committed, put it into bucket by tx fee_rate and confirmed blocks, +/// then decrease the count of unconfirmed txs. +/// +/// So we get a group of samples which includes txs count, average fee rate and confirmed blocks, etc. +/// For estimate, we loop through each bucket, calculate the confirmed txs rate, until meet the required_confirm_rate. +#[derive(Clone)] +struct TxConfirmStat { + min_fee_rate: FeeRate, + /// per bucket stat + bucket_stats: Vec, + /// bucket upper bound fee_rate => bucket index + fee_rate_to_bucket: BTreeMap, + /// confirm_blocks => bucket index => confirmed txs count + confirm_blocks_to_confirmed_txs: Vec>, + /// confirm_blocks => bucket index => failed txs count + confirm_blocks_to_failed_txs: Vec>, + /// Track recent N blocks unconfirmed txs + /// tracked block index => bucket index => TxTracker + block_unconfirmed_txs: Vec>, + decay_factor: f64, +} + +#[derive(Clone)] +struct TxRecord { + height: u64, + bucket_index: usize, + fee_rate: FeeRate, +} + +/// Estimator track new block and tx_pool to collect data +/// we track every new tx enter txpool and record the tip height and fee_rate, +/// when tx is packed into a new block or dropped by txpool, +/// we get a sample about how long a tx with X fee_rate can get confirmed or get dropped. +/// +/// In inner, we group samples by predefined fee_rate buckets. +/// To estimator fee_rate for a confirm target(how many blocks that a tx can get committed), +/// we travel through fee_rate buckets, try to find a fee_rate X to let a tx get committed +/// with high probilities within confirm target blocks. +/// +#[derive(Clone)] +pub struct Algorithm { + best_height: u64, + start_height: u64, + /// a data struct to track tx confirm status + tx_confirm_stat: TxConfirmStat, + tracked_txs: HashMap, + + current_tip: BlockNumber, + is_ready: bool, +} + +impl BucketStat { + // add a new fee rate to this bucket + fn new_fee_rate_sample(&mut self, fee_rate: FeeRate) { + self.txs_count += 1f64; + let total_fee_rate = self + .total_fee_rate + .as_u64() + .saturating_add(fee_rate.as_u64()); + self.total_fee_rate = FeeRate::from_u64(total_fee_rate); + } + + // get average fee rate from a bucket + fn avg_fee_rate(&self) -> Option { + if self.txs_count > 0f64 { + Some(FeeRate::from_u64( + ((self.total_fee_rate.as_u64() as f64) / self.txs_count) as u64, + )) + } else { + None + } + } +} + +impl Default for TxConfirmStat { + fn default() -> Self { + let min_bucket_feerate = f64::from(constants::LOWEST_FEE_RATE.as_u64() as u32); + // MULTIPLE = max_bucket_feerate / min_bucket_feerate + const MULTIPLE: f64 = 10000.0; + let max_bucket_feerate = min_bucket_feerate * MULTIPLE; + // expect 200 buckets + let fee_spacing = (MULTIPLE.ln() / 200.0f64).exp(); + // half life each 100 blocks, math.exp(math.log(0.5) / 100) + let decay_factor: f64 = (0.5f64.ln() / 100.0).exp(); + + let mut buckets = Vec::new(); + let mut bucket_fee_boundary = min_bucket_feerate; + // initialize fee_rate buckets + while bucket_fee_boundary <= max_bucket_feerate { + buckets.push(FeeRate::from_u64(bucket_fee_boundary as u64)); + bucket_fee_boundary *= fee_spacing; + } + Self::new(buckets, MAX_CONFIRM_BLOCKS, decay_factor) + } +} + +impl TxConfirmStat { + fn new(buckets: Vec, max_confirm_blocks: usize, decay_factor: f64) -> Self { + // max_confirm_blocsk: The number of blocks that the esitmator will trace the statistics. + let min_fee_rate = buckets[0]; + let bucket_stats = vec![BucketStat::default(); buckets.len()]; + let confirm_blocks_to_confirmed_txs = vec![vec![0f64; buckets.len()]; max_confirm_blocks]; + let confirm_blocks_to_failed_txs = vec![vec![0f64; buckets.len()]; max_confirm_blocks]; + let block_unconfirmed_txs = vec![vec![0; buckets.len()]; max_confirm_blocks]; + let fee_rate_to_bucket = buckets + .into_iter() + .enumerate() + .map(|(i, fee_rate)| (fee_rate, i)) + .collect(); + TxConfirmStat { + min_fee_rate, + bucket_stats, + fee_rate_to_bucket, + block_unconfirmed_txs, + confirm_blocks_to_confirmed_txs, + confirm_blocks_to_failed_txs, + decay_factor, + } + } + + /// Return upper bound fee_rate bucket + /// assume we have three buckets with fee_rate [1.0, 2.0, 3.0], we return index 1 for fee_rate 1.5 + fn bucket_index_by_fee_rate(&self, fee_rate: FeeRate) -> Option { + self.fee_rate_to_bucket + .range(fee_rate..) + .next() + .map(|(_fee_rate, index)| *index) + } + + fn max_confirms(&self) -> usize { + self.confirm_blocks_to_confirmed_txs.len() + } + + // add confirmed sample + fn add_confirmed_tx(&mut self, blocks_to_confirm: usize, fee_rate: FeeRate) { + if blocks_to_confirm < 1 { + return; + } + let bucket_index = match self.bucket_index_by_fee_rate(fee_rate) { + Some(index) => index, + None => return, + }; + // increase txs_count in buckets + for i in (blocks_to_confirm - 1)..self.max_confirms() { + self.confirm_blocks_to_confirmed_txs[i][bucket_index] += 1f64; + } + let stat = &mut self.bucket_stats[bucket_index]; + stat.new_fee_rate_sample(fee_rate); + } + + // track an unconfirmed tx + // entry_height - tip number when tx enter txpool + fn add_unconfirmed_tx(&mut self, entry_height: u64, fee_rate: FeeRate) -> Option { + let bucket_index = match self.bucket_index_by_fee_rate(fee_rate) { + Some(index) => index, + None => return None, + }; + let block_index = (entry_height % (self.block_unconfirmed_txs.len() as u64)) as usize; + self.block_unconfirmed_txs[block_index][bucket_index] += 1; + Some(bucket_index) + } + + fn remove_unconfirmed_tx( + &mut self, + entry_height: u64, + tip_height: u64, + bucket_index: usize, + count_failure: bool, + ) { + let tx_age = tip_height.saturating_sub(entry_height) as usize; + if tx_age < 1 { + return; + } + if tx_age >= self.block_unconfirmed_txs.len() { + self.bucket_stats[bucket_index].old_unconfirmed_txs -= 1; + } else { + let block_index = (entry_height % self.block_unconfirmed_txs.len() as u64) as usize; + self.block_unconfirmed_txs[block_index][bucket_index] -= 1; + } + if count_failure { + self.confirm_blocks_to_failed_txs[tx_age - 1][bucket_index] += 1f64; + } + } + + fn move_track_window(&mut self, height: u64) { + let block_index = (height % (self.block_unconfirmed_txs.len() as u64)) as usize; + for bucket_index in 0..self.bucket_stats.len() { + // mark unconfirmed txs as old_unconfirmed_txs + self.bucket_stats[bucket_index].old_unconfirmed_txs += + self.block_unconfirmed_txs[block_index][bucket_index]; + self.block_unconfirmed_txs[block_index][bucket_index] = 0; + } + } + + /// apply decay factor on stats, smoothly reduce the effects of old samples. + fn decay(&mut self) { + let decay_factor = self.decay_factor; + for (bucket_index, bucket) in self.bucket_stats.iter_mut().enumerate() { + self.confirm_blocks_to_confirmed_txs + .iter_mut() + .for_each(|buckets| { + buckets[bucket_index] *= decay_factor; + }); + + self.confirm_blocks_to_failed_txs + .iter_mut() + .for_each(|buckets| { + buckets[bucket_index] *= decay_factor; + }); + bucket.total_fee_rate = + FeeRate::from_u64((bucket.total_fee_rate.as_u64() as f64 * decay_factor) as u64); + bucket.txs_count *= decay_factor; + // TODO do we need decay the old unconfirmed? + } + } + + /// The naive estimate implementation + /// 1. find best range of buckets satisfy the given condition + /// 2. get median fee_rate from best range bucekts + fn estimate_median( + &self, + confirm_blocks: usize, + required_samples: usize, + required_confirm_rate: f64, + ) -> Result { + // A tx need 1 block to propose, then 2 block to get confirmed + // so at least confirm blocks is 3 blocks. + if confirm_blocks < 3 || required_samples == 0 { + ckb_logger::debug!( + "confirm_blocks(={}) < 3 || required_samples(={}) == 0", + confirm_blocks, + required_samples + ); + return Err(Error::LackData); + } + let mut confirmed_txs = 0f64; + let mut txs_count = 0f64; + let mut failure_count = 0f64; + let mut extra_count = 0; + let mut best_bucket_start = 0; + let mut best_bucket_end = 0; + let mut start_bucket_index = 0; + let mut find_best = false; + // try find enough sample data from buckets + for (bucket_index, stat) in self.bucket_stats.iter().enumerate() { + confirmed_txs += self.confirm_blocks_to_confirmed_txs[confirm_blocks - 1][bucket_index]; + failure_count += self.confirm_blocks_to_failed_txs[confirm_blocks - 1][bucket_index]; + extra_count += &self.block_unconfirmed_txs[confirm_blocks - 1][bucket_index]; + txs_count += stat.txs_count; + // we have enough data + while txs_count as usize >= required_samples { + let confirm_rate = confirmed_txs / (txs_count + failure_count + extra_count as f64); + // satisfied required_confirm_rate, find the best buckets range + if confirm_rate >= required_confirm_rate { + best_bucket_start = start_bucket_index; + best_bucket_end = bucket_index; + find_best = true; + break; + } else { + // remove sample data of the first bucket in the range, then retry + let stat = &self.bucket_stats[start_bucket_index]; + confirmed_txs -= self.confirm_blocks_to_confirmed_txs[confirm_blocks - 1] + [start_bucket_index]; + failure_count -= + self.confirm_blocks_to_failed_txs[confirm_blocks - 1][start_bucket_index]; + extra_count -= + &self.block_unconfirmed_txs[confirm_blocks - 1][start_bucket_index]; + txs_count -= stat.txs_count; + start_bucket_index += 1; + continue; + } + } + + // end loop if we found the best buckets + if find_best { + break; + } + } + + if find_best { + let best_range_txs_count: f64 = self.bucket_stats[best_bucket_start..=best_bucket_end] + .iter() + .map(|b| b.txs_count) + .sum(); + + // find median bucket + if best_range_txs_count != 0f64 { + let mut half_count = best_range_txs_count / 2f64; + for bucket in &self.bucket_stats[best_bucket_start..=best_bucket_end] { + // find the median bucket + if bucket.txs_count >= half_count { + return bucket + .avg_fee_rate() + .map(|fee_rate| cmp::max(fee_rate, self.min_fee_rate)) + .ok_or(Error::NoProperFeeRate); + } else { + half_count -= bucket.txs_count; + } + } + } + ckb_logger::trace!("no best fee rate"); + } else { + ckb_logger::trace!("no best bucket"); + } + + Err(Error::NoProperFeeRate) + } +} + +impl Default for Algorithm { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm { + /// Creates a new estimator. + pub fn new() -> Self { + Self { + best_height: 0, + start_height: 0, + tx_confirm_stat: Default::default(), + tracked_txs: Default::default(), + current_tip: 0, + is_ready: false, + } + } + + fn process_block_tx(&mut self, height: u64, tx_hash: &Byte32) -> bool { + if let Some(tx) = self.drop_tx_inner(tx_hash, false) { + let blocks_to_confirm = height.saturating_sub(tx.height) as usize; + self.tx_confirm_stat + .add_confirmed_tx(blocks_to_confirm, tx.fee_rate); + true + } else { + // tx is not tracked + false + } + } + + /// process new block + /// record confirm blocks for txs which we tracked before. + fn process_block(&mut self, height: u64, txs: impl Iterator) { + // For simpfy, we assume chain reorg will not effect tx fee. + if height <= self.best_height { + return; + } + self.best_height = height; + // update tx confirm stat + self.tx_confirm_stat.move_track_window(height); + self.tx_confirm_stat.decay(); + let processed_txs = txs.filter(|tx| self.process_block_tx(height, tx)).count(); + if self.start_height == 0 && processed_txs > 0 { + // start record + self.start_height = self.best_height; + ckb_logger::debug!("start recording at {}", self.start_height); + } + } + + /// track a tx that entered txpool + fn track_tx(&mut self, tx_hash: Byte32, fee_rate: FeeRate, height: u64) { + if self.tracked_txs.contains_key(&tx_hash) { + // already in track + return; + } + if height != self.best_height { + // ignore wrong height txs + return; + } + if let Some(bucket_index) = self.tx_confirm_stat.add_unconfirmed_tx(height, fee_rate) { + self.tracked_txs.insert( + tx_hash, + TxRecord { + height, + bucket_index, + fee_rate, + }, + ); + } + } + + fn drop_tx_inner(&mut self, tx_hash: &Byte32, count_failure: bool) -> Option { + self.tracked_txs.remove(tx_hash).map(|tx_record| { + self.tx_confirm_stat.remove_unconfirmed_tx( + tx_record.height, + self.best_height, + tx_record.bucket_index, + count_failure, + ); + tx_record + }) + } + + /// tx removed from txpool + fn drop_tx(&mut self, tx_hash: &Byte32) -> bool { + self.drop_tx_inner(tx_hash, true).is_some() + } + + /// estimate a fee rate for confirm target + fn estimate(&self, expect_confirm_blocks: BlockNumber) -> Result { + self.tx_confirm_stat.estimate_median( + expect_confirm_blocks as usize, + DEFAULT_MIN_SAMPLES, + DEFAULT_MIN_CONFIRM_RATE, + ) + } +} + +impl Algorithm { + pub fn update_ibd_state(&mut self, in_ibd: bool) { + if self.is_ready { + if in_ibd { + self.clear(); + self.is_ready = false; + } + } else if !in_ibd { + self.clear(); + self.is_ready = true; + } + } + + fn clear(&mut self) { + self.best_height = 0; + self.start_height = 0; + self.tx_confirm_stat = Default::default(); + self.tracked_txs.clear(); + self.current_tip = 0; + } + + pub fn commit_block(&mut self, block: &BlockView) { + let tip_number = block.number(); + self.current_tip = tip_number; + self.process_block(tip_number, block.tx_hashes().iter().map(ToOwned::to_owned)); + } + + pub fn accept_tx(&mut self, tx_hash: Byte32, info: TxEntryInfo) { + let weight = get_transaction_weight(info.size as usize, info.cycles); + let fee_rate = FeeRate::calculate(info.fee, weight); + self.track_tx(tx_hash, fee_rate, self.current_tip) + } + + pub fn reject_tx(&mut self, tx_hash: &Byte32) { + let _ = self.drop_tx(tx_hash); + } + + pub fn estimate_fee_rate(&self, target_blocks: BlockNumber) -> Result { + if !self.is_ready { + return Err(Error::NotReady); + } + self.estimate(target_blocks) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_estimate_median() { + let mut bucket_fee_rate = 1000; + let bucket_end_fee_rate = 5000; + let rate = 1.1f64; + // decay = exp(ln(0.5) / 100), so decay.pow(100) =~ 0.5 + let decay = 0.993f64; + let max_confirm_blocks = 1000; + // prepare fee rate buckets + let mut buckets = vec![]; + while bucket_fee_rate < bucket_end_fee_rate { + buckets.push(FeeRate::from_u64(bucket_fee_rate)); + bucket_fee_rate = (rate * bucket_fee_rate as f64) as u64; + } + let mut stat = TxConfirmStat::new(buckets, max_confirm_blocks, decay); + // txs data + let fee_rate_and_confirms = vec![ + (2500, 5), + (3000, 5), + (3500, 5), + (1500, 10), + (2000, 10), + (2100, 10), + (2200, 10), + (1200, 15), + (1000, 15), + ]; + for (fee_rate, blocks_to_confirm) in fee_rate_and_confirms { + stat.add_confirmed_tx(blocks_to_confirm, FeeRate::from_u64(fee_rate)); + } + // test basic median fee rate + assert_eq!( + stat.estimate_median(5, 3, 1f64), + Ok(FeeRate::from_u64(3000)) + ); + // test different required samples + assert_eq!( + stat.estimate_median(10, 1, 1f64), + Ok(FeeRate::from_u64(1500)) + ); + assert_eq!( + stat.estimate_median(10, 3, 1f64), + Ok(FeeRate::from_u64(2050)) + ); + assert_eq!( + stat.estimate_median(10, 4, 1f64), + Ok(FeeRate::from_u64(2050)) + ); + assert_eq!( + stat.estimate_median(15, 2, 1f64), + Ok(FeeRate::from_u64(1000)) + ); + assert_eq!( + stat.estimate_median(15, 3, 1f64), + Ok(FeeRate::from_u64(1200)) + ); + // test return zero if confirm_blocks or required_samples is zero + assert_eq!(stat.estimate_median(0, 4, 1f64), Err(Error::LackData)); + assert_eq!(stat.estimate_median(15, 0, 1f64), Err(Error::LackData)); + assert_eq!(stat.estimate_median(0, 3, 1f64), Err(Error::LackData)); + } +} diff --git a/util/fee-estimator/src/estimator/mod.rs b/util/fee-estimator/src/estimator/mod.rs new file mode 100644 index 0000000000..3c877beca8 --- /dev/null +++ b/util/fee-estimator/src/estimator/mod.rs @@ -0,0 +1,106 @@ +use std::sync::Arc; + +use ckb_types::{ + core::{ + tx_pool::{TxEntryInfo, TxPoolEntryInfo}, + BlockNumber, BlockView, EstimateMode, FeeRate, + }, + packed::Byte32, +}; +use ckb_util::RwLock; + +use crate::{constants, Error}; + +mod confirmation_fraction; +mod weight_units_flow; + +/// The fee estimator with a chosen algorithm. +#[derive(Clone)] +pub enum FeeEstimator { + /// Dummy fee estimate algorithm; just do nothing. + Dummy, + /// Confirmation fraction fee estimator algorithm. + ConfirmationFraction(Arc>), + /// Weight-Units flow fee estimator algorithm. + WeightUnitsFlow(Arc>), +} + +impl FeeEstimator { + /// Creates a new dummy fee estimator. + pub fn new_dummy() -> Self { + FeeEstimator::Dummy + } + + /// Creates a new confirmation fraction fee estimator. + pub fn new_confirmation_fraction() -> Self { + let algo = confirmation_fraction::Algorithm::new(); + FeeEstimator::ConfirmationFraction(Arc::new(RwLock::new(algo))) + } + + /// Target blocks for the provided estimate mode. + pub const fn target_blocks_for_estimate_mode(estimate_mode: EstimateMode) -> BlockNumber { + match estimate_mode { + EstimateMode::NoPriority => constants::DEFAULT_TARGET, + EstimateMode::LowPriority => constants::LOW_TARGET, + EstimateMode::MediumPriority => constants::MEDIUM_TARGET, + EstimateMode::HighPriority => constants::HIGH_TARGET, + } + } + + /// Creates a new weight-units flow fee estimator. + pub fn new_weight_units_flow() -> Self { + let algo = weight_units_flow::Algorithm::new(); + FeeEstimator::WeightUnitsFlow(Arc::new(RwLock::new(algo))) + } + + /// Updates the IBD state. + pub fn update_ibd_state(&self, in_ibd: bool) { + match self { + Self::Dummy => {} + Self::ConfirmationFraction(algo) => algo.write().update_ibd_state(in_ibd), + Self::WeightUnitsFlow(algo) => algo.write().update_ibd_state(in_ibd), + } + } + + /// Commits a block. + pub fn commit_block(&self, block: &BlockView) { + match self { + Self::Dummy => {} + Self::ConfirmationFraction(algo) => algo.write().commit_block(block), + Self::WeightUnitsFlow(algo) => algo.write().commit_block(block), + } + } + + /// Accepts a tx. + pub fn accept_tx(&self, tx_hash: Byte32, info: TxEntryInfo) { + match self { + Self::Dummy => {} + Self::ConfirmationFraction(algo) => algo.write().accept_tx(tx_hash, info), + Self::WeightUnitsFlow(algo) => algo.write().accept_tx(info), + } + } + + /// Rejects a tx. + pub fn reject_tx(&self, tx_hash: &Byte32) { + match self { + Self::Dummy | Self::WeightUnitsFlow(_) => {} + Self::ConfirmationFraction(algo) => algo.write().reject_tx(tx_hash), + } + } + + /// Estimates fee rate. + pub fn estimate_fee_rate( + &self, + estimate_mode: EstimateMode, + all_entry_info: TxPoolEntryInfo, + ) -> Result { + let target_blocks = Self::target_blocks_for_estimate_mode(estimate_mode); + match self { + Self::Dummy => Err(Error::Dummy), + Self::ConfirmationFraction(algo) => algo.read().estimate_fee_rate(target_blocks), + Self::WeightUnitsFlow(algo) => { + algo.read().estimate_fee_rate(target_blocks, all_entry_info) + } + } + } +} diff --git a/util/fee-estimator/src/estimator/weight_units_flow.rs b/util/fee-estimator/src/estimator/weight_units_flow.rs new file mode 100644 index 0000000000..a834d5a9b0 --- /dev/null +++ b/util/fee-estimator/src/estimator/weight_units_flow.rs @@ -0,0 +1,419 @@ +//! Weight-Units Flow Fee Estimator +//! +//! ### Summary +//! +//! This algorithm is migrated from a Bitcoin fee estimates algorithm. +//! +//! The original algorithm could be found in . +//! +//! ### Details +//! +//! #### Inputs +//! +//! The mempool is categorized into "fee buckets". +//! A bucket represents data about all transactions with a fee greater than or +//! equal to some amount (in `weight`). +//! +//! Each bucket contains 2 numeric values: +//! +//! - `current_weight`, represents the transactions currently sitting in the +//! mempool. +//! +//! - `flow`, represents the speed at which new transactions are entering the +//! mempool. +//! +//! It's sampled by observing the flow of transactions during twice the blocks +//! count of each target interval (ex: last 60 blocks for the 30 blocks target +//! interval). +//! +//! For simplicity, transactions are not looked at individually. +//! Focus is on the weight, like a fluid flowing from bucket to bucket. +//! +//! #### Computations +//! +//! Let's simulate what's going to happen during each timespan lasting blocks: +//! +//! - New transactions entering the mempool. +//! +//! While it's impossible to predict sudden changes to the speed at which new +//! weight is added to the mempool, for simplicty's sake we're going to assume +//! the flow we measured remains constant: `added_weight = flow * blocks`. +//! +//! - Transactions leaving the mempool due to mined blocks. Each block removes +//! up to `MAX_BLOCK_BYTES` weight from a bucket. +//! +//! Once we know the minimum expected number of blocks we can compute how that +//! would affect the bucket's weight: +//! `removed_weight = MAX_BLOCK_BYTES * blocks`. +//! +//! - Finally we can compute the expected final weight of the bucket: +//! `final_weight = current_weight + added_weight - removed_weight`. +//! +//! The cheapest bucket whose `final_weight` is less than or equal to 0 is going +//! to be the one selected as the estimate. + +use std::collections::HashMap; + +use ckb_chain_spec::consensus::MAX_BLOCK_BYTES; +use ckb_types::core::{ + tx_pool::{get_transaction_weight, TxEntryInfo, TxPoolEntryInfo}, + BlockNumber, BlockView, FeeRate, +}; + +use crate::{constants, Error}; + +const FEE_RATE_UNIT: u64 = 1000; + +#[derive(Clone)] +pub struct Algorithm { + boot_tip: BlockNumber, + current_tip: BlockNumber, + txs: HashMap>, + + is_ready: bool, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct TxStatus { + weight: u64, + fee_rate: FeeRate, +} + +impl PartialOrd for TxStatus { + fn partial_cmp(&self, other: &TxStatus) -> Option<::std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for TxStatus { + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + self.fee_rate + .cmp(&other.fee_rate) + .then_with(|| other.weight.cmp(&self.weight)) + } +} + +impl TxStatus { + fn new_from_entry_info(info: TxEntryInfo) -> Self { + let weight = get_transaction_weight(info.size as usize, info.cycles); + let fee_rate = FeeRate::calculate(info.fee, weight); + Self { weight, fee_rate } + } +} + +impl Default for Algorithm { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm { + pub fn new() -> Self { + Self { + boot_tip: 0, + current_tip: 0, + txs: Default::default(), + is_ready: false, + } + } + + pub fn update_ibd_state(&mut self, in_ibd: bool) { + if self.is_ready { + if in_ibd { + self.clear(); + self.is_ready = false; + } + } else if !in_ibd { + self.clear(); + self.is_ready = true; + } + } + + fn clear(&mut self) { + self.boot_tip = 0; + self.current_tip = 0; + self.txs.clear(); + } + + pub fn commit_block(&mut self, block: &BlockView) { + let tip_number = block.number(); + if self.boot_tip == 0 { + self.boot_tip = tip_number; + } + self.current_tip = tip_number; + self.expire(); + } + + fn expire(&mut self) { + let historical_blocks = Self::historical_blocks(constants::MAX_TARGET); + let expired_tip = self.current_tip.saturating_sub(historical_blocks); + self.txs.retain(|&num, _| num >= expired_tip); + } + + pub fn accept_tx(&mut self, info: TxEntryInfo) { + if self.current_tip == 0 { + return; + } + let item = TxStatus::new_from_entry_info(info); + self.txs + .entry(self.current_tip) + .and_modify(|items| items.push(item)) + .or_insert_with(|| vec![item]); + } + + pub fn estimate_fee_rate( + &self, + target_blocks: BlockNumber, + all_entry_info: TxPoolEntryInfo, + ) -> Result { + if !self.is_ready { + return Err(Error::NotReady); + } + + let sorted_current_txs = { + let mut current_txs: Vec<_> = all_entry_info + .pending + .into_values() + .chain(all_entry_info.proposed.into_values()) + .map(TxStatus::new_from_entry_info) + .collect(); + current_txs.sort_unstable_by(|a, b| b.cmp(a)); + current_txs + }; + + self.do_estimate(target_blocks, &sorted_current_txs) + } +} + +impl Algorithm { + fn do_estimate( + &self, + target_blocks: BlockNumber, + sorted_current_txs: &[TxStatus], + ) -> Result { + ckb_logger::debug!( + "boot: {}, current: {}, target: {target_blocks} blocks", + self.boot_tip, + self.current_tip, + ); + let historical_blocks = Self::historical_blocks(target_blocks); + ckb_logger::debug!("required: {historical_blocks} blocks"); + if historical_blocks > self.current_tip.saturating_sub(self.boot_tip) { + return Err(Error::LackData); + } + + let max_fee_rate = if let Some(fee_rate) = sorted_current_txs.first().map(|tx| tx.fee_rate) + { + fee_rate + } else { + return Ok(constants::LOWEST_FEE_RATE); + }; + + ckb_logger::debug!("max fee rate of current transactions: {max_fee_rate}"); + + let max_bucket_index = Self::max_bucket_index_by_fee_rate(max_fee_rate); + ckb_logger::debug!("current weight buckets size: {}", max_bucket_index + 1); + + // Create weight buckets. + let current_weight_buckets = { + let mut buckets = vec![0u64; max_bucket_index + 1]; + let mut index_curr = max_bucket_index; + for tx in sorted_current_txs { + let index = Self::max_bucket_index_by_fee_rate(tx.fee_rate); + if index < index_curr { + let weight_curr = buckets[index_curr]; + for i in buckets.iter_mut().take(index_curr) { + *i = weight_curr; + } + } + buckets[index] += tx.weight; + index_curr = index; + } + let weight_curr = buckets[index_curr]; + for i in buckets.iter_mut().take(index_curr) { + *i = weight_curr; + } + buckets + }; + for (index, weight) in current_weight_buckets.iter().enumerate() { + if *weight != 0 { + ckb_logger::trace!(">>> current_weight[{index}]: {weight}"); + } + } + + // Calculate flow speeds for buckets. + let flow_speed_buckets = { + let historical_tip = self.current_tip - historical_blocks; + let sorted_flowed = self.sorted_flowed(historical_tip); + let mut buckets = vec![0u64; max_bucket_index + 1]; + let mut index_curr = max_bucket_index; + for tx in &sorted_flowed { + let index = Self::max_bucket_index_by_fee_rate(tx.fee_rate); + if index > max_bucket_index { + continue; + } + if index < index_curr { + let flowed_curr = buckets[index_curr]; + for i in buckets.iter_mut().take(index_curr) { + *i = flowed_curr; + } + } + buckets[index] += tx.weight; + index_curr = index; + } + let flowed_curr = buckets[index_curr]; + for i in buckets.iter_mut().take(index_curr) { + *i = flowed_curr; + } + buckets + .into_iter() + .map(|value| value / historical_blocks) + .collect::>() + }; + for (index, speed) in flow_speed_buckets.iter().enumerate() { + if *speed != 0 { + ckb_logger::trace!(">>> flow_speed[{index}]: {speed}"); + } + } + + for bucket_index in 1..=max_bucket_index { + let current_weight = current_weight_buckets[bucket_index]; + let added_weight = flow_speed_buckets[bucket_index] * target_blocks; + // Note: blocks are not full even there are many pending transactions, + // since `MAX_BLOCK_PROPOSALS_LIMIT = 1500`. + let removed_weight = (MAX_BLOCK_BYTES * 85 / 100) * target_blocks; + let passed = current_weight + added_weight <= removed_weight; + ckb_logger::trace!( + ">>> bucket[{}]: {}; {} + {} - {}", + bucket_index, + passed, + current_weight, + added_weight, + removed_weight + ); + if passed { + let fee_rate = Self::lowest_fee_rate_by_bucket_index(bucket_index); + return Ok(fee_rate); + } + } + + Err(Error::NoProperFeeRate) + } + + fn sorted_flowed(&self, historical_tip: BlockNumber) -> Vec { + let mut statuses: Vec<_> = self + .txs + .iter() + .filter(|(&num, _)| num >= historical_tip) + .flat_map(|(_, statuses)| statuses.to_owned()) + .collect(); + statuses.sort_unstable_by(|a, b| b.cmp(a)); + ckb_logger::trace!(">>> sorted flowed length: {}", statuses.len()); + statuses + } +} + +impl Algorithm { + fn historical_blocks(target_blocks: BlockNumber) -> BlockNumber { + if target_blocks < constants::MIN_TARGET { + constants::MIN_TARGET * 2 + } else { + target_blocks * 2 + } + } + + fn lowest_fee_rate_by_bucket_index(index: usize) -> FeeRate { + let t = FEE_RATE_UNIT; + let value = match index as u64 { + // 0->0 + 0 => 0, + // 1->1000, 2->2000, .., 10->10000 + x if x <= 10 => t * x, + // 11->12000, 12->14000, .., 30->50000 + x if x <= 30 => t * (10 + (x - 10) * 2), + // 31->55000, 32->60000, ..., 60->200000 + x if x <= 60 => t * (10 + 20 * 2 + (x - 30) * 5), + // 61->210000, 62->220000, ..., 90->500000 + x if x <= 90 => t * (10 + 20 * 2 + 30 * 5 + (x - 60) * 10), + // 91->520000, 92->540000, ..., 115 -> 1000000 + x if x <= 115 => t * (10 + 20 * 2 + 30 * 5 + 30 * 10 + (x - 90) * 20), + // 116->1050000, 117->1100000, ..., 135->2000000 + x if x <= 135 => t * (10 + 20 * 2 + 30 * 5 + 30 * 10 + 25 * 20 + (x - 115) * 50), + // 136->2100000, 137->2200000, ... + x => t * (10 + 20 * 2 + 30 * 5 + 30 * 10 + 25 * 20 + 20 * 50 + (x - 135) * 100), + }; + FeeRate::from_u64(value) + } + + fn max_bucket_index_by_fee_rate(fee_rate: FeeRate) -> usize { + let t = FEE_RATE_UNIT; + let index = match fee_rate.as_u64() { + x if x <= 10_000 => x / t, + x if x <= 50_000 => (x + t * 10) / (2 * t), + x if x <= 200_000 => (x + t * 100) / (5 * t), + x if x <= 500_000 => (x + t * 400) / (10 * t), + x if x <= 1_000_000 => (x + t * 1_300) / (20 * t), + x if x <= 2_000_000 => (x + t * 4_750) / (50 * t), + x => (x + t * 11_500) / (100 * t), + }; + index as usize + } +} + +#[cfg(test)] +mod tests { + use super::Algorithm; + use ckb_types::core::FeeRate; + + #[test] + fn test_bucket_index_and_fee_rate_expected() { + let testdata = [ + (0, 0), + (1, 1_000), + (2, 2_000), + (10, 10_000), + (11, 12_000), + (12, 14_000), + (30, 50_000), + (31, 55_000), + (32, 60_000), + (60, 200_000), + (61, 210_000), + (62, 220_000), + (90, 500_000), + (91, 520_000), + (92, 540_000), + (115, 1_000_000), + (116, 1_050_000), + (117, 1_100_000), + (135, 2_000_000), + (136, 2_100_000), + (137, 2_200_000), + ]; + for (bucket_index, fee_rate) in &testdata[..] { + let expected_fee_rate = + Algorithm::lowest_fee_rate_by_bucket_index(*bucket_index).as_u64(); + assert_eq!(expected_fee_rate, *fee_rate); + let actual_bucket_index = + Algorithm::max_bucket_index_by_fee_rate(FeeRate::from_u64(*fee_rate)); + assert_eq!(actual_bucket_index, *bucket_index); + } + } + + #[test] + fn test_bucket_index_and_fee_rate_continuous() { + for fee_rate in 0..3_000_000 { + let bucket_index = Algorithm::max_bucket_index_by_fee_rate(FeeRate::from_u64(fee_rate)); + let fee_rate_le = Algorithm::lowest_fee_rate_by_bucket_index(bucket_index).as_u64(); + let fee_rate_gt = Algorithm::lowest_fee_rate_by_bucket_index(bucket_index + 1).as_u64(); + assert!( + fee_rate_le <= fee_rate && fee_rate < fee_rate_gt, + "Error for bucket[{}]: {} <= {} < {}", + bucket_index, + fee_rate_le, + fee_rate, + fee_rate_gt, + ); + } + } +} diff --git a/util/fee-estimator/src/lib.rs b/util/fee-estimator/src/lib.rs new file mode 100644 index 0000000000..e2c444e35b --- /dev/null +++ b/util/fee-estimator/src/lib.rs @@ -0,0 +1,8 @@ +//! CKB's built-in fee estimator, which shares data with the ckb node through the tx-pool service. + +pub mod constants; +pub(crate) mod error; +pub(crate) mod estimator; + +pub use error::Error; +pub use estimator::FeeEstimator; diff --git a/util/jsonrpc-types/src/fee_estimator.rs b/util/jsonrpc-types/src/fee_estimator.rs new file mode 100644 index 0000000000..0c1821d061 --- /dev/null +++ b/util/jsonrpc-types/src/fee_estimator.rs @@ -0,0 +1,46 @@ +use ckb_types::core; +use serde::{Deserialize, Serialize}; + +use schemars::JsonSchema; + +/// The fee estimate mode. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum EstimateMode { + /// No priority, expect the transaction to be committed in 1 hour. + NoPriority, + /// Low priority, expect the transaction to be committed in 30 minutes. + LowPriority, + /// Medium priority, expect the transaction to be committed in 10 minutes. + MediumPriority, + /// High priority, expect the transaction to be committed as soon as possible. + HighPriority, +} + +impl Default for EstimateMode { + fn default() -> Self { + Self::NoPriority + } +} + +impl From for core::EstimateMode { + fn from(json: EstimateMode) -> Self { + match json { + EstimateMode::NoPriority => core::EstimateMode::NoPriority, + EstimateMode::LowPriority => core::EstimateMode::LowPriority, + EstimateMode::MediumPriority => core::EstimateMode::MediumPriority, + EstimateMode::HighPriority => core::EstimateMode::HighPriority, + } + } +} + +impl From for EstimateMode { + fn from(data: core::EstimateMode) -> Self { + match data { + core::EstimateMode::NoPriority => EstimateMode::NoPriority, + core::EstimateMode::LowPriority => EstimateMode::LowPriority, + core::EstimateMode::MediumPriority => EstimateMode::MediumPriority, + core::EstimateMode::HighPriority => EstimateMode::HighPriority, + } + } +} diff --git a/util/jsonrpc-types/src/lib.rs b/util/jsonrpc-types/src/lib.rs index ac70de3ec3..c05ab01db6 100644 --- a/util/jsonrpc-types/src/lib.rs +++ b/util/jsonrpc-types/src/lib.rs @@ -6,6 +6,7 @@ mod bytes; mod cell; mod debug; mod experiment; +mod fee_estimator; mod fee_rate; mod fixed_bytes; mod indexer; @@ -37,6 +38,7 @@ pub use self::bytes::JsonBytes; pub use self::cell::{CellData, CellInfo, CellWithStatus}; pub use self::debug::{ExtraLoggerConfig, MainLoggerConfig}; pub use self::experiment::{DaoWithdrawingCalculationKind, EstimateCycles}; +pub use self::fee_estimator::EstimateMode; pub use self::fee_rate::FeeRateDef; pub use self::fixed_bytes::Byte32; pub use self::info::{ChainInfo, DeploymentInfo, DeploymentPos, DeploymentState, DeploymentsInfo}; diff --git a/util/launcher/src/lib.rs b/util/launcher/src/lib.rs index 2c21a3291d..87e87a2a2f 100644 --- a/util/launcher/src/lib.rs +++ b/util/launcher/src/lib.rs @@ -206,6 +206,7 @@ impl Launcher { .sync_config(self.args.config.network.sync.clone()) .header_map_tmp_dir(self.args.config.tmp_dir.clone()) .block_assembler_config(block_assembler_config) + .fee_estimator_config(self.args.config.fee_estimator.clone()) .build()?; // internal check migrate_version diff --git a/util/types/src/core/fee_estimator.rs b/util/types/src/core/fee_estimator.rs new file mode 100644 index 0000000000..e93ada5bf7 --- /dev/null +++ b/util/types/src/core/fee_estimator.rs @@ -0,0 +1,18 @@ +/// The fee estimate mode. +#[derive(Clone, Copy, Debug)] +pub enum EstimateMode { + /// No priority, expect the transaction to be committed in 1 hour. + NoPriority, + /// Low priority, expect the transaction to be committed in 30 minutes. + LowPriority, + /// Medium priority, expect the transaction to be committed in 10 minutes. + MediumPriority, + /// High priority, expect the transaction to be committed as soon as possible. + HighPriority, +} + +impl Default for EstimateMode { + fn default() -> Self { + Self::NoPriority + } +} diff --git a/util/types/src/core/mod.rs b/util/types/src/core/mod.rs index 5b0cbdb5b3..8b7fc8dcd1 100644 --- a/util/types/src/core/mod.rs +++ b/util/types/src/core/mod.rs @@ -23,6 +23,7 @@ mod tests; mod advanced_builders; mod blockchain; mod extras; +mod fee_estimator; mod fee_rate; mod reward; mod transaction_meta; @@ -31,6 +32,7 @@ mod views; pub use advanced_builders::{BlockBuilder, HeaderBuilder, TransactionBuilder}; pub use blockchain::DepType; pub use extras::{BlockExt, EpochExt, EpochNumberWithFraction, TransactionInfo}; +pub use fee_estimator::EstimateMode; pub use fee_rate::FeeRate; pub use reward::{BlockEconomicState, BlockIssuance, BlockReward, MinerReward}; pub use transaction_meta::{TransactionMeta, TransactionMetaBuilder};