Skip to content

Commit

Permalink
feat: replace fee rate estimator with mempool.space client
Browse files Browse the repository at this point in the history
Signed-off-by: Philipp Hoenisch <[email protected]>
  • Loading branch information
bonomat committed Feb 6, 2024
1 parent f3490a5 commit 78457ff
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 52 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ln-dlc-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ lightning-rapid-gossip-sync = { version = "0.0.117" }
lightning-transaction-sync = { version = "0.0.117", features = ["esplora-blocking"] }
ln-dlc-storage = { path = "../../crates/ln-dlc-storage" }
log = "0.4.17"
mempool = { path = "../../crates/mempool" }
p2pd-oracle-client = { version = "0.1.0" }
parking_lot = { version = "0.12.1" }
rand = "0.8.5"
Expand Down
97 changes: 46 additions & 51 deletions crates/ln-dlc-node/src/fee_rate_estimator.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
use anyhow::Result;
use bdk::FeeRate;
use bitcoin::Network;
use lightning::chain::chaininterface::ConfirmationTarget;
use lightning::chain::chaininterface::FeeEstimator;
use lightning::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
use parking_lot::RwLock;
use std::collections::HashMap;

const CONFIRMATION_TARGETS: [(ConfirmationTarget, usize); 4] = [
// We choose an extremely high background confirmation target to avoid force-closing channels
// unnecessarily.
(ConfirmationTarget::Background, 1008),
// We just want to end up in the mempool eventually. We just set the target to 1008
// as that is esplora's highest block target available
(ConfirmationTarget::MempoolMinimum, 1008),
(ConfirmationTarget::Normal, 6),
(ConfirmationTarget::HighPriority, 3),
];

/// Default values used when constructing the [`FeeRateEstimator`] if the fee rate sever cannot give
/// us up-to-date values.
///
/// In sats/kwu.
const FEE_RATE_DEFAULTS: [(ConfirmationTarget, u32); 3] = [
(ConfirmationTarget::Background, FEERATE_FLOOR_SATS_PER_KW),
(ConfirmationTarget::Normal, 2000),
(ConfirmationTarget::HighPriority, 5000),
const FEE_RATE_DEFAULTS: [(ConfirmationTarget, u32); 4] = [
(ConfirmationTarget::MempoolMinimum, 1000),
(ConfirmationTarget::Background, 2000),
(ConfirmationTarget::Normal, 3000),
(ConfirmationTarget::HighPriority, 4000),
];

pub struct FeeRateEstimator {
client: esplora_client::BlockingClient,
client: mempool::MempoolFeeRateEstimator,
fee_rate_cache: RwLock<HashMap<ConfirmationTarget, FeeRate>>,
}

Expand All @@ -42,31 +33,27 @@ impl EstimateFeeRate for FeeRateEstimator {
}
}

fn to_mempool_network(value: Network) -> mempool::Network {
match value {
Network::Bitcoin => mempool::Network::Mainnet,
Network::Testnet => mempool::Network::Testnet,
Network::Signet => mempool::Network::Signet,
Network::Regtest => mempool::Network::Local,
}
}

impl FeeRateEstimator {
/// Constructor for the [`FeeRateEstimator`].
pub fn new(esplora_url: String) -> Self {
let client = esplora_client::BlockingClient::from_agent(esplora_url, ureq::agent());

let initial_fee_rates = match client.get_fee_estimates() {
Ok(estimates) => {
HashMap::from_iter(CONFIRMATION_TARGETS.into_iter().map(|(target, n_blocks)| {
let fee_rate = esplora_client::convert_fee_rate(n_blocks, estimates.clone())
.expect("fee rates for our confirmation targets");
let fee_rate = FeeRate::from_sat_per_vb(fee_rate);

(target, fee_rate)
}))
}
Err(e) => {
tracing::warn!(defaults = ?FEE_RATE_DEFAULTS, "Initializing fee rate cache with default values: {e:#}");

HashMap::from_iter(
FEE_RATE_DEFAULTS.into_iter().map(|(target, fee_rate)| {
(target, FeeRate::from_sat_per_kwu(fee_rate as f32))
}),
)
}
};
pub fn new(network: Network) -> Self {
let client = mempool::MempoolFeeRateEstimator::new(to_mempool_network(network));

tracing::warn!(defaults = ?FEE_RATE_DEFAULTS, "Initializing fee rate cache with default values.");

let initial_fee_rates = HashMap::from_iter(
FEE_RATE_DEFAULTS
.into_iter()
.map(|(target, fee_rate)| (target, FeeRate::from_sat_per_kwu(fee_rate as f32))),
);

let fee_rate_cache = RwLock::new(initial_fee_rates);

Expand All @@ -85,21 +72,29 @@ impl FeeRateEstimator {
}

pub(crate) async fn update(&self) -> Result<()> {
let estimates = self.client.get_fee_estimates()?;
let estimates = self.client.fetch_fee().await?;

let mut locked_fee_rate_cache = self.fee_rate_cache.write();
for (target, n_blocks) in CONFIRMATION_TARGETS {
let fee_rate = esplora_client::convert_fee_rate(n_blocks, estimates.clone())?;

let fee_rate = FeeRate::from_sat_per_vb(fee_rate);

locked_fee_rate_cache.insert(target, fee_rate);
tracing::trace!(
n_blocks_confirmation = %n_blocks,
sats_per_kwu = %fee_rate.fee_wu(1000),
"Updated fee rate estimate",
);
}
locked_fee_rate_cache.insert(
ConfirmationTarget::MempoolMinimum,
FeeRate::from_sat_per_vb(estimates.minimum_fee as f32),
);

locked_fee_rate_cache.insert(
ConfirmationTarget::Background,
FeeRate::from_sat_per_vb(estimates.economy_fee as f32),
);

locked_fee_rate_cache.insert(
ConfirmationTarget::Normal,
FeeRate::from_sat_per_vb(estimates.hour_fee as f32),
);

locked_fee_rate_cache.insert(
ConfirmationTarget::HighPriority,
FeeRate::from_sat_per_vb(estimates.fastest_fee as f32),
);

Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ln-dlc-node/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ impl<S: TenTenOneStorage + 'static, N: Storage + Sync + Send + 'static> Node<S,
let dlc_storage = Arc::new(DlcStorageProvider::new(storage.clone()));
let ln_storage = Arc::new(storage);

let fee_rate_estimator = Arc::new(FeeRateEstimator::new(esplora_server_url.clone()));
let fee_rate_estimator = Arc::new(FeeRateEstimator::new(network));
let ln_dlc_wallet = {
Arc::new(LnDlcWallet::new(
esplora_client.clone(),
Expand Down
15 changes: 15 additions & 0 deletions crates/mempool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "mempool"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1"
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
116 changes: 116 additions & 0 deletions crates/mempool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use anyhow::Result;
use serde::Deserialize;

const MEMPOOL_FEE_RATE_URL_MAINNET: &str = "https://mempool.space";
const MEMPOOL_FEE_RATE_URL_SIGNET: &str = "https://mempool.space/signet";
const MEMPOOL_FEE_RATE_URL_TESTNET: &str = "https://mempool.space/testnet";

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FeeRate {
pub fastest_fee: usize,
pub half_hour_fee: usize,
pub hour_fee: usize,
pub economy_fee: usize,
pub minimum_fee: usize,
}

impl FeeRate {
fn local_fee_rate() -> Self {
FeeRate {
// we on purpose have different values to see an effect for clients asking for different
// priorities
fastest_fee: 5,
half_hour_fee: 4,
hour_fee: 3,
economy_fee: 2,
minimum_fee: 1,
}
}
}

#[derive(PartialEq)]
pub enum Network {
Mainnet,
Signet,
Testnet,
/// We assume a local regtest setup and will not perform any request to mempool.space
Local,
}

pub struct MempoolFeeRateEstimator {
url: String,
network: Network,
}

impl MempoolFeeRateEstimator {
pub fn new(network: Network) -> Self {
let url = match network {
Network::Mainnet => MEMPOOL_FEE_RATE_URL_MAINNET,
Network::Signet => MEMPOOL_FEE_RATE_URL_SIGNET,
Network::Testnet => MEMPOOL_FEE_RATE_URL_TESTNET,
Network::Local => "http://thereisnosuchthingasabitcoinmempool.com",
}
.to_string();

Self { url, network }
}

pub async fn fetch_fee(&self) -> Result<FeeRate> {
if Network::Local == self.network {
return Ok(FeeRate::local_fee_rate());
}
let client = reqwest::Client::new();
let url = format!("{}/api/v1/fees/recommended", self.url);
let response = client.get(url).send().await?;
let fee_rate = response.json().await?;
Ok(fee_rate)
}

/// Fetches the latest fee rate from mempool.space
///
/// Note: if self.network is Network::Local we will not perform a request to mempool.space but
/// return static values
pub fn fetch_fee_sync(&self) -> Result<FeeRate> {
if Network::Local == self.network {
return Ok(FeeRate::local_fee_rate());
}
let url = format!("{}/api/v1/fees/recommended", self.url);
let response = reqwest::blocking::get(url)?;
let fee_rate = response.json()?;
Ok(fee_rate)
}
}

#[cfg(test)]
mod tests {
use super::*;

// we keep this test running on CI even though it connects to the internet. This allows us to
// be notified if the API ever changes
#[tokio::test]
pub async fn test_fetching_fee_rate_from_mempool() {
let mempool = MempoolFeeRateEstimator::new(Network::Testnet);
let _testnet_fee_rate = mempool.fetch_fee().await.unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Mainnet);
let _testnet_fee_rate = mempool.fetch_fee().await.unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Signet);
let _testnet_fee_rate = mempool.fetch_fee().await.unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Local);
let _testnet_fee_rate = mempool.fetch_fee().await.unwrap();
}

// we keep this test running on CI even though it connects to the internet. This allows us to
// be notified if the API ever changes
#[test]
pub fn test_fetching_fee_rate_from_mempool_sync() {
let mempool = MempoolFeeRateEstimator::new(Network::Testnet);
let _testnet_fee_rate = mempool.fetch_fee_sync().unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Mainnet);
let _testnet_fee_rate = mempool.fetch_fee_sync().unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Signet);
let _testnet_fee_rate = mempool.fetch_fee_sync().unwrap();
let mempool = MempoolFeeRateEstimator::new(Network::Local);
let _testnet_fee_rate = mempool.fetch_fee_sync().unwrap();
}
}

0 comments on commit 78457ff

Please sign in to comment.