Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace fee rate estimator with mempool.space client #1916

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"] }
88 changes: 88 additions & 0 deletions crates/mempool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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)
}
}

#[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();
}
}
Loading