diff --git a/Cargo.lock b/Cargo.lock
index f1bfa18325..6603fdd4a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -884,6 +884,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"
@@ -1527,6 +1538,7 @@ dependencies = [
"ckb-db",
"ckb-db-schema",
"ckb-error",
+ "ckb-fee-estimator",
"ckb-logger",
"ckb-metrics",
"ckb-migrate",
@@ -1694,6 +1706,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 26698b6d50..44794dedcf 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"}
ckb-util = { path = "../util", version = "= 0.119.0-pre" }
ckb-metrics = { path = "../util/metrics", version = "= 0.119.0-pre" }
bitflags = "1.0"
diff --git a/shared/src/shared_builder.rs b/shared/src/shared_builder.rs
index d5644cacb5..47f0a85e4e 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,
})
}
@@ -198,6 +202,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,
})
@@ -247,6 +252,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;
@@ -362,6 +373,7 @@ impl SharedBuilder {
block_assembler_config,
notify_config,
async_handle,
+ fee_estimator_config,
header_map_tmp_dir,
} = self;
@@ -401,6 +413,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),
@@ -408,9 +431,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());
@@ -497,7 +525,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();
@@ -508,10 +540,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();
@@ -535,7 +572,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);
}
}
@@ -543,6 +582,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 cc42e7b52f..d7245d5a5d 100644
--- a/tx-pool/Cargo.toml
+++ b/tx-pool/Cargo.toml
@@ -46,6 +46,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..628f764263
--- /dev/null
+++ b/util/fee-estimator/src/estimator/confirmation_fraction.rs
@@ -0,0 +1,555 @@
+//! 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).inspect(|tx_record| {
+ self.tx_confirm_stat.remove_unconfirmed_tx(
+ tx_record.height,
+ self.best_height,
+ tx_record.bucket_index,
+ count_failure,
+ );
+ })
+ }
+
+ /// 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};