Skip to content

Commit

Permalink
Introduce OnchainFeeEstimator
Browse files Browse the repository at this point in the history
We also decouple fee estimation from the BDK on-chain wallet and BDK's
corresponding `EsploraBlockchain`. Instead we use the esplora client
directly in a dedicated `OnchainFeeEstimator` object.

For one, this change is nice as it allows us
to move more things out of the `Wallet` thread and corresponding locks.
Moreover, it makes things more modular in general, which makes future
upgrades and testing easier.
  • Loading branch information
tnull committed Dec 4, 2023
1 parent 372a520 commit ea31d2d
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 158 deletions.
1 change: 1 addition & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ enum NodeError {
"ChannelClosingFailed",
"ChannelConfigUpdateFailed",
"PersistenceFailed",
"FeerateEstimationUpdateFailed",
"WalletOperationFailed",
"OnchainTxSigningFailed",
"MessageSigningFailed",
Expand Down
19 changes: 13 additions & 6 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::event::EventQueue;
use crate::fee_estimator::OnchainFeeEstimator;
use crate::gossip::GossipSource;
use crate::io;
use crate::io::sqlite_store::SqliteStore;
Expand Down Expand Up @@ -465,7 +466,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
BuildError::WalletSetupFailed
})?;

let (blockchain, tx_sync, tx_broadcaster) = match chain_data_source_config {
let (blockchain, tx_sync, tx_broadcaster, fee_estimator) = match chain_data_source_config {
Some(ChainDataSourceConfig::Esplora(server_url)) => {
let tx_sync = Arc::new(EsploraSyncClient::new(server_url.clone(), Arc::clone(&logger)));
let blockchain =
Expand All @@ -475,7 +476,9 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
tx_sync.client().clone(),
Arc::clone(&logger),
));
(blockchain, tx_sync, tx_broadcaster)
let fee_estimator =
Arc::new(OnchainFeeEstimator::new(tx_sync.client().clone(), Arc::clone(&logger)));
(blockchain, tx_sync, tx_broadcaster, fee_estimator)
}
None => {
// Default to Esplora client.
Expand All @@ -488,7 +491,9 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
tx_sync.client().clone(),
Arc::clone(&logger),
));
(blockchain, tx_sync, tx_broadcaster)
let fee_estimator =
Arc::new(OnchainFeeEstimator::new(tx_sync.client().clone(), Arc::clone(&logger)));
(blockchain, tx_sync, tx_broadcaster, fee_estimator)
}
};

Expand All @@ -497,6 +502,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
blockchain,
bdk_wallet,
Arc::clone(&tx_broadcaster),
Arc::clone(&fee_estimator),
Arc::clone(&logger),
));

Expand All @@ -505,7 +511,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
Some(Arc::clone(&tx_sync)),
Arc::clone(&tx_broadcaster),
Arc::clone(&logger),
Arc::clone(&wallet),
Arc::clone(&fee_estimator),
Arc::clone(&kv_store),
));

Expand Down Expand Up @@ -605,7 +611,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&wallet),
Arc::clone(&fee_estimator),
Arc::clone(&chain_monitor),
Arc::clone(&tx_broadcaster),
Arc::clone(&router),
Expand All @@ -629,7 +635,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
best_block: BestBlock::new(genesis_block_hash, 0),
};
channelmanager::ChannelManager::new(
Arc::clone(&wallet),
Arc::clone(&fee_estimator),
Arc::clone(&chain_monitor),
Arc::clone(&tx_broadcaster),
Arc::clone(&router),
Expand Down Expand Up @@ -781,6 +787,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
wallet,
tx_sync,
tx_broadcaster,
fee_estimator,
event_queue,
channel_manager,
chain_monitor,
Expand Down
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub enum Error {
ChannelConfigUpdateFailed,
/// Persistence failed.
PersistenceFailed,
/// A fee rate estimation update failed.
FeerateEstimationUpdateFailed,
/// A wallet operation failed.
WalletOperationFailed,
/// A signing operation for transaction failed.
Expand Down Expand Up @@ -79,6 +81,9 @@ impl fmt::Display for Error {
Self::ChannelClosingFailed => write!(f, "Failed to close channel."),
Self::ChannelConfigUpdateFailed => write!(f, "Failed to update channel config."),
Self::PersistenceFailed => write!(f, "Failed to persist data."),
Self::FeerateEstimationUpdateFailed => {
write!(f, "Failed to update fee rate estimates.")
}
Self::WalletOperationFailed => write!(f, "Failed to conduct wallet operation."),
Self::OnchainTxSigningFailed => write!(f, "Failed to sign given transaction."),
Self::MessageSigningFailed => write!(f, "Failed to sign given message."),
Expand Down
17 changes: 11 additions & 6 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::types::{Broadcaster, Wallet};
use crate::types::{Broadcaster, FeeEstimator, Wallet};
use crate::{
hex_utils, ChannelManager, Config, Error, KeysManager, NetworkGraph, PeerInfo, PeerStore,
UserChannelId,
Expand All @@ -14,7 +14,9 @@ use crate::io::{
};
use crate::logger::{log_debug, log_error, log_info, Logger};

use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator};
use lightning::chain::chaininterface::{
BroadcasterInterface, ConfirmationTarget, FeeEstimator as LDKFeeEstimator,
};
use lightning::events::Event as LdkEvent;
use lightning::events::PaymentPurpose;
use lightning::impl_writeable_tlv_based_enum;
Expand Down Expand Up @@ -248,6 +250,7 @@ where
wallet: Arc<Wallet>,
channel_manager: Arc<ChannelManager<K>>,
tx_broadcaster: Arc<Broadcaster>,
fee_estimator: Arc<FeeEstimator>,
network_graph: Arc<NetworkGraph>,
keys_manager: Arc<KeysManager>,
payment_store: Arc<PaymentStore<K, L>>,
Expand All @@ -264,15 +267,17 @@ where
pub fn new(
event_queue: Arc<EventQueue<K, L>>, wallet: Arc<Wallet>,
channel_manager: Arc<ChannelManager<K>>, tx_broadcaster: Arc<Broadcaster>,
network_graph: Arc<NetworkGraph>, keys_manager: Arc<KeysManager>,
payment_store: Arc<PaymentStore<K, L>>, peer_store: Arc<PeerStore<K, L>>,
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, logger: L, config: Arc<Config>,
fee_estimator: Arc<FeeEstimator>, network_graph: Arc<NetworkGraph>,
keys_manager: Arc<KeysManager>, payment_store: Arc<PaymentStore<K, L>>,
peer_store: Arc<PeerStore<K, L>>, runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
logger: L, config: Arc<Config>,
) -> Self {
Self {
event_queue,
wallet,
channel_manager,
tx_broadcaster,
fee_estimator,
network_graph,
keys_manager,
payment_store,
Expand Down Expand Up @@ -589,7 +594,7 @@ where

let output_descriptors = &outputs.iter().collect::<Vec<_>>();
let tx_feerate = self
.wallet
.fee_estimator
.get_est_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee);

// We set nLockTime to the current height to discourage fee sniping.
Expand Down
131 changes: 131 additions & 0 deletions src/fee_estimator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use crate::logger::{log_error, log_trace, Logger};
use crate::Error;

use lightning::chain::chaininterface::{
ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW,
};

use bdk::FeeRate;
use esplora_client::AsyncClient as EsploraClient;

use std::collections::HashMap;
use std::ops::Deref;
use std::sync::RwLock;

pub(crate) struct OnchainFeeEstimator<L: Deref>
where
L::Target: Logger,
{
fee_rate_cache: RwLock<HashMap<ConfirmationTarget, FeeRate>>,
esplora_client: EsploraClient,
logger: L,
}

impl<L: Deref> OnchainFeeEstimator<L>
where
L::Target: Logger,
{
pub(crate) fn new(esplora_client: EsploraClient, logger: L) -> Self {
let fee_rate_cache = RwLock::new(HashMap::new());
Self { fee_rate_cache, esplora_client, logger }
}

pub(crate) async fn update_fee_estimates(&self) -> Result<(), Error> {
let confirmation_targets = vec![
ConfirmationTarget::OnChainSweep,
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee,
ConfirmationTarget::AnchorChannelFee,
ConfirmationTarget::NonAnchorChannelFee,
ConfirmationTarget::ChannelCloseMinimum,
];
for target in confirmation_targets {
let num_blocks = match target {
ConfirmationTarget::OnChainSweep => 6,
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => 1,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => 1008,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => 144,
ConfirmationTarget::AnchorChannelFee => 1008,
ConfirmationTarget::NonAnchorChannelFee => 12,
ConfirmationTarget::ChannelCloseMinimum => 144,
};

let estimates = self.esplora_client.get_fee_estimates().await.map_err(|e| {
log_error!(
self.logger,
"Failed to retrieve fee rate estimates for {:?}: {}",
target,
e
);
Error::FeerateEstimationUpdateFailed
})?;

let converted_estimates = esplora_client::convert_fee_rate(num_blocks, estimates)
.map_err(|e| {
log_error!(
self.logger,
"Failed to convert fee rate estimates for {:?}: {}",
target,
e
);
Error::FeerateEstimationUpdateFailed
})?;

let fee_rate = FeeRate::from_sat_per_vb(converted_estimates);

// LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that
// require some post-estimation adjustments to the fee rates, which we do here.
let adjusted_fee_rate = match target {
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => {
let really_high_prio = fee_rate.as_sat_per_vb() * 10.0;
FeeRate::from_sat_per_vb(really_high_prio)
}
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => {
let slightly_less_than_background = fee_rate.fee_wu(1000) - 250;
FeeRate::from_sat_per_kwu(slightly_less_than_background as f32)
}
_ => fee_rate,
};

let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
locked_fee_rate_cache.insert(target, adjusted_fee_rate);
log_trace!(
self.logger,
"Fee rate estimation updated for {:?}: {} sats/kwu",
target,
adjusted_fee_rate.fee_wu(1000)
);
}
Ok(())
}

pub(crate) fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();

let fallback_sats_kwu = match confirmation_target {
ConfirmationTarget::OnChainSweep => 5000,
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => 25 * 250,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
ConfirmationTarget::AnchorChannelFee => 500,
ConfirmationTarget::NonAnchorChannelFee => 1000,
ConfirmationTarget::ChannelCloseMinimum => 500,
};

// We'll fall back on this, if we really don't have any other information.
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);

*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
}
}

impl<L: Deref> FeeEstimator for OnchainFeeEstimator<L>
where
L::Target: Logger,
{
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
.max(FEERATE_FLOOR_SATS_PER_KW)
}
}
Loading

0 comments on commit ea31d2d

Please sign in to comment.