Skip to content

Commit 8963226

Browse files
committed
Add configuration knob to enable 0FC channels
In upcoming commits we will read this knob to determine whether to negotiate 0FC channels. For now, we make a best-effort attempt to make sure the configured chain source supports 0FC channels if this knob is set. Do this roundtrip at the same time we make a roundtrip to retrieve the feerates to keep startup as fast as possible.
1 parent fac8a1f commit 8963226

8 files changed

Lines changed: 237 additions & 7 deletions

File tree

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ enum NodeError {
246246
"LnurlAuthFailed",
247247
"LnurlAuthTimeout",
248248
"InvalidLnurl",
249+
"ChainSourceNotSupported",
249250
};
250251

251252
typedef dictionary NodeStatus;

src/chain/bitcoind.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@ impl BitcoindChainSource {
119119
self.api_client.utxo_source()
120120
}
121121

122+
pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
123+
let node_version_result = tokio::time::timeout(
124+
Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS),
125+
self.api_client.get_node_version(),
126+
)
127+
.await
128+
.map_err(|e| {
129+
log_error!(self.logger, "Failed to get node version: {:?}", e);
130+
Error::ConnectionFailed
131+
})?;
132+
133+
let node_version = node_version_result.map_err(|e| {
134+
log_error!(self.logger, "Failed to get node version: {:?}", e);
135+
Error::ConnectionFailed
136+
})?;
137+
138+
// v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral dust
139+
if node_version < 290000 {
140+
log_error!(self.logger, "Bitcoin backend MUST be greater than or equal to v29");
141+
return Err(Error::ChainSourceNotSupported);
142+
}
143+
Ok(())
144+
}
145+
122146
pub(super) async fn continuously_sync_wallets(
123147
&self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>,
124148
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
@@ -748,6 +772,31 @@ impl BitcoindClient {
748772
}
749773
}
750774

775+
pub(crate) async fn get_node_version(&self) -> Result<u64, BitcoindClientError> {
776+
match self {
777+
BitcoindClient::Rpc { rpc_client, .. } => {
778+
Self::get_node_version_inner(Arc::clone(rpc_client))
779+
.await
780+
.map_err(BitcoindClientError::Rpc)
781+
},
782+
BitcoindClient::Rest { rpc_client, .. } => {
783+
// Bitcoin Core's REST interface does not support `getnetworkinfo`
784+
// so we use the RPC client.
785+
Self::get_node_version_inner(Arc::clone(rpc_client))
786+
.await
787+
.map_err(BitcoindClientError::Rpc)
788+
},
789+
}
790+
}
791+
792+
async fn get_node_version_inner(rpc_client: Arc<RpcClient>) -> Result<u64, RpcClientError> {
793+
rpc_client.call_method::<serde_json::Value>("getnetworkinfo", &[]).await.and_then(|value| {
794+
value["version"].as_u64().ok_or(RpcClientError::InvalidData(String::from(
795+
"The version field in the `getnetworkinfo` response should be a u64",
796+
)))
797+
})
798+
}
799+
751800
/// Broadcasts the provided transaction.
752801
pub(crate) async fn broadcast_transaction(
753802
&self, tx: &Transaction,

src/chain/electrum.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,53 @@ impl ElectrumChainSource {
306306
Ok(())
307307
}
308308

309+
pub(crate) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
310+
let electrum_client: Arc<ElectrumRuntimeClient> = if let Some(client) =
311+
self.electrum_runtime_status.read().expect("lock").client().as_ref()
312+
{
313+
Arc::clone(client)
314+
} else {
315+
debug_assert!(
316+
false,
317+
"We should have started the chain source before checking submitpackage support"
318+
);
319+
return Err(Error::ConnectionFailed);
320+
};
321+
322+
// TODO: Use `protocol_version` API once shipped in
323+
// https://github.com/bitcoindevkit/rust-electrum-client/pull/213.
324+
//
325+
// This could still accept an Electrum server running against Bitcoin Core v26
326+
// through v28, which does not relay ephemeral dust.
327+
let spawn_fut = electrum_client.runtime.spawn_blocking({
328+
let electrum_client = Arc::clone(&electrum_client.electrum_client);
329+
move || electrum_client.transaction_broadcast_package(&super::dummy_package())
330+
});
331+
let timeout_fut = tokio::time::timeout(
332+
Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs),
333+
spawn_fut,
334+
);
335+
336+
match timeout_fut.await {
337+
Ok(Ok(Ok(_))) => Ok(()),
338+
Ok(Ok(Err(
339+
e @ (electrum_client::Error::Protocol(_)
340+
| electrum_client::Error::AllAttemptsErrored(_)),
341+
))) => {
342+
log_error!(self.logger, "Electrum server does not support submitpackage: {:?}", e);
343+
Err(Error::ChainSourceNotSupported)
344+
},
345+
e => {
346+
log_error!(
347+
self.logger,
348+
"Failed to check support for submitpackage on the Electrum server: {:?}",
349+
e
350+
);
351+
Err(Error::ConnectionFailed)
352+
},
353+
}
354+
}
355+
309356
pub(crate) async fn process_broadcast_package(&self, package: Vec<Transaction>) {
310357
let electrum_client: Arc<ElectrumRuntimeClient> = if let Some(client) =
311358
self.electrum_runtime_status.read().expect("lock").client().as_ref()

src/chain/esplora.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@ impl EsploraChainSource {
8383
})
8484
}
8585

86+
pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
87+
// This could still accept an Esplora server running against Bitcoin Core v26
88+
// through v28, which does not relay ephemeral dust.
89+
self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err(
90+
|e| {
91+
if let esplora_client::Error::HttpResponse { status: 404, message } = e {
92+
log_error!(
93+
self.logger,
94+
"Esplora server does not support submitpackage: {}",
95+
message
96+
);
97+
Error::ChainSourceNotSupported
98+
} else {
99+
log_error!(
100+
self.logger,
101+
"Failed to check support for submitpackage on the Esplora server: {}",
102+
e
103+
);
104+
Error::ConnectionFailed
105+
}
106+
},
107+
)?;
108+
Ok(())
109+
}
110+
86111
pub(super) async fn sync_onchain_wallet(
87112
&self, onchain_wallet: Arc<Wallet>,
88113
) -> Result<(), Error> {

src/chain/mod.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ use crate::runtime::Runtime;
2929
use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
3030
use crate::{Error, PersistedNodeMetrics};
3131

32+
/// We use this parent-child TRUC package to make sure the configured chain source supports
33+
/// broadcasting packages via the `submitpackage` Bitcoin Core RPC.
34+
const PARENT_TXID: &str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696";
35+
const PARENT_HEX: &str =
36+
"0300000000010160d0cdb72f2ddf719f40ca32f44614c67577fc75996140544003915683c34a310000000000fd\
37+
ffffff0201000000000000000451024e73876100000000000022512042731375894dad3b25092cd0f713dc5bee4\
38+
a71e30a95e1db3d880906d7eba1fa01409327942924218e4eb1635a7cce6706fcb37b8bbb61a2f0b86357356681\
39+
4e09419a3501e02252043bb237d479304632282fe9159db9e9a6ae6ec5bedea9f0f115a97b0e00";
40+
const CHILD_TXID: &str = "d011b3ff78cdfb8b93822639ea87771847936b04bb83afc8763a7c02a386ae26";
41+
const CHILD_HEX: &str =
42+
"0300000000010296f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0000000000ff\
43+
ffffff96f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0100000000fdffffff015\
44+
660000000000000225120ac18cd599a1be003595854e2eeec18dbe1c92d04b0ba05812d04445e3fcf16bc000140\
45+
1462a35808d77a164f0a23a84c4721d1545befd09ad19945bb8aa0ea5576953a9699038725f944b1bc429942ef4\
46+
7e6504a554babf022cb15db53be2d8c1dbfe5a97b0e00";
47+
48+
fn dummy_package() -> [bitcoin::Transaction; 2] {
49+
use bitcoin::consensus::Decodable;
50+
use bitcoin::hex::FromHex;
51+
use bitcoin::Transaction;
52+
let parent_tx_bytes = Vec::from_hex(PARENT_HEX).expect("read from a constant");
53+
let child_tx_bytes = Vec::from_hex(CHILD_HEX).expect("read from a constant");
54+
let parent =
55+
Transaction::consensus_decode(&mut &parent_tx_bytes[..]).expect("read from a constant");
56+
let child =
57+
Transaction::consensus_decode(&mut &child_tx_bytes[..]).expect("read from a constant");
58+
assert_eq!(parent.compute_txid().to_string(), PARENT_TXID);
59+
assert_eq!(child.compute_txid().to_string(), CHILD_TXID);
60+
[parent, child]
61+
}
62+
3263
pub(crate) enum WalletSyncStatus {
3364
Completed,
3465
InProgress { subscribers: tokio::sync::broadcast::Sender<Result<(), Error>> },
@@ -438,6 +469,26 @@ impl ChainSource {
438469
}
439470
}
440471

472+
pub(crate) async fn validate_zero_fee_commitments_support_if_required(
473+
&self, zero_fee_commitments_support_required: bool,
474+
) -> Result<(), Error> {
475+
if !zero_fee_commitments_support_required {
476+
return Ok(());
477+
}
478+
479+
match &self.kind {
480+
ChainSourceKind::Esplora(esplora_chain_source) => {
481+
esplora_chain_source.validate_zero_fee_commitments_support().await
482+
},
483+
ChainSourceKind::Electrum(electrum_chain_source) => {
484+
electrum_chain_source.validate_zero_fee_commitments_support().await
485+
},
486+
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
487+
bitcoind_chain_source.validate_zero_fee_commitments_support().await
488+
},
489+
}
490+
}
491+
441492
pub(crate) async fn continuously_process_broadcast_queue(
442493
&self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>,
443494
) {

src/config.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ pub const DEFAULT_LOG_FILENAME: &'static str = "ldk_node.log";
6060
/// The default storage directory.
6161
pub const DEFAULT_STORAGE_DIR_PATH: &str = "/tmp/ldk_node";
6262

63-
// The default Esplora server we're using.
63+
// The default Esplora server we're using. It supports `submitpackage`, check using POST on the
64+
// `/txs/package` endpoint.
6465
pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api";
6566

6667
/// The default stop gap used for BDK full scans of the on-chain wallet.
@@ -305,10 +306,11 @@ impl Default for HumanReadableNamesConfig {
305306
///
306307
/// ### Defaults
307308
///
308-
/// | Parameter | Value |
309-
/// |----------------------------|--------|
310-
/// | `trusted_peers_no_reserve` | [] |
311-
/// | `per_channel_reserve_sats` | 25000 |
309+
/// | Parameter | Value |
310+
/// |-------------------------------|--------|
311+
/// | `trusted_peers_no_reserve` | [] |
312+
/// | `per_channel_reserve_sats` | 25000 |
313+
/// | `enable_zero_fee_commitments` | false |
312314
///
313315
///
314316
/// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions
@@ -344,13 +346,29 @@ pub struct AnchorChannelsConfig {
344346
/// might not suffice to successfully spend the Anchor output and have the HTLC transactions
345347
/// confirmed on-chain, i.e., you may want to adjust this value accordingly.
346348
pub per_channel_reserve_sats: u64,
349+
/// If set, we will first attempt to negotiate `option_zero_fee_commitments` before falling
350+
/// back to `option_anchors_zero_fee_htlc_tx` and `option_static_remotekey`, as supported by
351+
/// the peer. Zero-fee commitment channels remove all commitment feerate negotiation from
352+
/// the channel, which eliminates a very common source of channel force-closures. These
353+
/// channels instead source *all* the fees required to confirm the commitment from the
354+
/// anchor reserve of the channel closer at the time of force-close. If set, your chain
355+
/// source *must* support the `submitpackage` Bitcoin Core RPC, and relay [TRUC], [P2A],
356+
/// and [Ephemeral Dust].
357+
/// See [BOLT 3] for more technical details.
358+
///
359+
/// [TRUC]: https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki
360+
/// [P2A]: https://github.com/bitcoin/bips/blob/master/bip-0433.mediawiki
361+
/// [Ephemeral Dust]: https://bitcoincore.org/en/releases/29.0
362+
/// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#shared_anchor-output-zero_fee_commitments
363+
pub enable_zero_fee_commitments: bool,
347364
}
348365

349366
impl Default for AnchorChannelsConfig {
350367
fn default() -> Self {
351368
Self {
352369
trusted_peers_no_reserve: Vec::new(),
353370
per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS,
371+
enable_zero_fee_commitments: false,
354372
}
355373
}
356374
}

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ pub enum Error {
137137
LnurlAuthTimeout,
138138
/// The provided lnurl is invalid.
139139
InvalidLnurl,
140+
/// The configured chain source is not supported.
141+
ChainSourceNotSupported,
140142
}
141143

142144
impl fmt::Display for Error {
@@ -222,6 +224,9 @@ impl fmt::Display for Error {
222224
Self::LnurlAuthFailed => write!(f, "LNURL-auth authentication failed."),
223225
Self::LnurlAuthTimeout => write!(f, "LNURL-auth authentication timed out."),
224226
Self::InvalidLnurl => write!(f, "The provided lnurl is invalid."),
227+
Self::ChainSourceNotSupported => {
228+
write!(f, "The configured chain source is not supported.")
229+
},
225230
}
226231
}
227232
}

src/lib.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,43 @@ impl Node {
291291
e
292292
})?;
293293

294-
// Block to ensure we update our fee rate cache once on startup
294+
let manager_owns_any_0fc_channels =
295+
self.channel_manager.list_channels().into_iter().any(|channel| {
296+
channel
297+
.channel_shutdown_state
298+
.map_or(true, |s| s != ChannelShutdownState::ShutdownComplete)
299+
&& channel
300+
.channel_type
301+
.as_ref()
302+
.map_or(false, |c| c.requires_anchor_zero_fee_commitments())
303+
});
304+
let monitor_owns_any_0fc_channels =
305+
self.chain_monitor.list_monitors().into_iter().any(|channel_id| {
306+
self.chain_monitor
307+
.get_monitor(channel_id)
308+
.map(|monitor| {
309+
monitor.channel_type_features().requires_anchor_zero_fee_commitments()
310+
})
311+
.unwrap_or(false)
312+
});
313+
let zero_fee_commitments_support_required = manager_owns_any_0fc_channels
314+
|| monitor_owns_any_0fc_channels
315+
|| self.config.anchor_channels_config.enable_zero_fee_commitments;
316+
317+
// Block to ensure we update our fee rate cache once on startup.
318+
// Also take this opportunity to make sure our chain source supports 0FC channels
319+
// if they are enabled.
320+
//
321+
// TODO: drop 0FC chain source validation when support is ubiquitous
295322
let chain_source = Arc::clone(&self.chain_source);
296-
self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?;
323+
self.runtime.block_on(async move {
324+
tokio::try_join!(
325+
chain_source.update_fee_rate_estimates(),
326+
chain_source.validate_zero_fee_commitments_support_if_required(
327+
zero_fee_commitments_support_required
328+
)
329+
)
330+
})?;
297331

298332
// Spawn background task continuously syncing onchain, lightning, and fee rate cache.
299333
let stop_sync_receiver = self.stop_sender.subscribe();

0 commit comments

Comments
 (0)