Skip to content

Zero-fee commitments support#660

Open
tankyleo wants to merge 15 commits into
lightningdevkit:mainfrom
tankyleo:25-10-0fc-channel-config
Open

Zero-fee commitments support#660
tankyleo wants to merge 15 commits into
lightningdevkit:mainfrom
tankyleo:25-10-0fc-channel-config

Conversation

@tankyleo

@tankyleo tankyleo commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

No description provided.

@ldk-reviews-bot

ldk-reviews-bot commented Oct 13, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tankyleo tankyleo requested a review from tnull October 13, 2025 13:14
Comment thread src/config.rs Outdated
/// `option_anchor_zero_fee_commitments`. All the caveats and warnings in
/// [`AnchorChannelsConfig`] still apply.
/// [`AnchorChannelsConfig`]: Config::anchor_channels_config
pub enable_zero_fee_commitments: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll wan to add a new flag here that's probably hard to understand for the user? Rather, shouldn't we enable this for the user based on our current 'trust model settings' here?

Also, from these docs it's very unclear what this setting even does, when the user would want to enable it, what drawbacks it has, etc

@tnull tnull Oct 13, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, thinking about it again it seems that we should never set negotiate_anchor_zero_fee_commitments until we're positive our chain sources support submitpackage/TRUC, no? And once we are positive, we would always set it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather, shouldn't we enable this for the user based on our current 'trust model settings' here?

Don't quite follow here could you expand ? I think 0FC channels merit an explicit setting somewhere rather than derived from trust model settings.

Also, from these docs it's very unclear what this setting even does, when the user would want to enable it, what drawbacks it has, etc

Yes will expand

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't quite follow here could you expand ? I think 0FC channels merit an explicit setting somewhere rather than derived from trust model settings.

Why, what do they fundamentally change for the user compared to our three current modes (fully trusted/keep 0-reserve, still try to claim/keep X reserve, try to claim)? Keep in mind that communicating these three modes to the user is already very hard, they always have a very hard time understanding what this means. Now, how would we communicate any changed assumptions for 0FC here? If we already trust our counterparty already, wouldn't we always want to enable 0FC for the UX improvements?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why, what do they fundamentally change for the user compared to our three current modes (fully trusted/keep 0-reserve, still try to claim/keep X reserve, try to claim)?

Let me see I don't think they change anything ? Whether to enable or disable 0FC channels is orthogonal to these modes ie trusted_peers_no_reserve and per_channel_reserve_sats should have no influence on whether we enable 0FC channels (only that per_channel_reserve_sats should be set to some value). I suspect you don't agree :)

If we already trust our counterparty already, wouldn't we always want to enable 0FC for the UX improvements?

It seems to me trusting our counterparty -> keeping 0 reserve is orthogonal to whether the user wants to enable 0FC channels ? for example a user trusts their counterparty, but wants to wait for greater adoption of Core v29+ before using 0FC channels.

@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from cb1cdf9 to c874049 Compare October 24, 2025 06:05
@tankyleo tankyleo requested a review from tnull October 24, 2025 06:06
@tankyleo tankyleo marked this pull request as draft October 24, 2025 06:06
@tankyleo

Copy link
Copy Markdown
Contributor Author

Marked as draft: I think we should wait for electrum and esplora submit package support before merging this PR.

@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from c874049 to ef3ba7a Compare October 27, 2025 05:15
@tankyleo

tankyleo commented Oct 27, 2025

Copy link
Copy Markdown
Contributor Author

Successfully opened some 0FC channels, made payments, and force closed them with the esplora diff in this branch.

https://mutinynet.com/tx/508a954d85f5b7daf224a2fdc54ea6de9a26c0f62f7d58284bf61c3cdfd346e6

@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from ef3ba7a to 3ebd017 Compare October 29, 2025 08:56
@tankyleo tankyleo changed the title Add AnchorChannelsConfig::enable_zero_fee_commitments Zero-fee commitments support Oct 30, 2025
@tankyleo tankyleo self-assigned this Oct 30, 2025
@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from 3ebd017 to eda13d4 Compare November 11, 2025 04:16
@tankyleo tankyleo removed this from Weekly Goals Nov 12, 2025
@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch 4 times, most recently from e136f33 to d4a2a04 Compare November 19, 2025 19:11
@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch 9 times, most recently from 771f45b to a7c7911 Compare May 29, 2026 00:36
@tankyleo tankyleo marked this pull request as ready for review May 29, 2026 01:08
@tankyleo tankyleo requested a review from benthecarman May 29, 2026 01:08
@tankyleo

Copy link
Copy Markdown
Contributor Author

Rebased, squashed the previous fixups, added a few more

Comment thread src/chain/esplora.rs
}

pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially if we already submitted this package before, couldn't the backend return an different error that we'd misinterpret as 'doesn't support submitpackage'? E.g., typical transaction broadcast errors are 'already known' or 'missing-inputs'. Why can't we ever hit them here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked down the stack, and also had codex review the mempool-electrs and blockstream-electrs code.

already-known and missing-inputs errors get placed in the package_msg and error fields of the response, but still yield status 200.

Non 200 error codes are for transport-level errors, not for errors returned in the fields of the submitpackage response. As long as Bitcoin Core RPC returns some valid response for submitpackage, we get back 200.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if we always get back a 200 if there's no connectivity error, then I'm still confused why we interpret any error here as Esplora server does not support submitpackage? Shouldn't we only match on 404 (assuming that's what's returned if the endpoint isn't known?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also want to note that this way of checking support for Electrum/Esplora is error prone as it will pass if the service runs bitcoind v26+ I believe, while we require v29+ in reality?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we only match on 404 (assuming that's what's returned if the endpoint isn't known?)

Sounds good yes I will be more precise.

Also want to note that this way of checking support for Electrum/Esplora is error prone as it will pass if the service runs bitcoind v26+ I believe, while we require v29+ in reality?

Yes agreed this is not a guarantee we are safe. Do you see another way to do this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more prevision below based on the error returned across both electrum and esplora.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also want to note that this way of checking support for Electrum/Esplora is error prone as it will pass if the service runs bitcoind v26+ I believe, while we require v29+ in reality?

Thought about it further, I'm open to all options here.

Is it too early / reckless to ship 0FC channels against electrum / esplora given that they offer no way to assert that the backend Bitcoind is v29+ ?

Should we wait for a way to assert more cleanly that some esplora server supports submitpackage? You have a PR to do this for the electrum client thank you !

Should we drop the validate_zero_fee_commitments_support startup roundtrip in electrum and esplora given that it doesn't even guarantee that the backend bitcoind will relay ephemeral dust?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I also note, mempool-electrs HTTP esplora responses add a x-bitcoin-version to all response headers, containing the bitcoin version of the node it's running against. This is not the case yet for blockstream-electrs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes agreed this is not a guarantee we are safe. Do you see another way to do this?

No, not sure either. At the very least we need to make sure our docs clearly say this, and we should probably also add a compatibility note in the (pending) changelog.

Is it too early / reckless to ship 0FC channels against electrum / esplora given that they offer no way to assert that the backend Bitcoind is v29+ ?

Hmm, not necessarily, though maybe we should check what the big explorers are running right now? Are we positive mempool and Blockstream are on v29+?

Should we drop the validate_zero_fee_commitments_support startup roundtrip in electrum and esplora given that it doesn't even guarantee that the backend bitcoind will relay ephemeral dust?

Good question that I'm also not entirely sure about. Given the call should be pretty quick and runs in parallel to the blocking fee rate update we shouldn't lose much time? But we just need to make sure it's as robust as possible, and maybe even add a TODO/issue to drop it again in a few months when we're positive the support is ubiquitous?

ah I also note, mempool-electrs HTTP esplora responses add a x-bitcoin-version to all response headers, containing the bitcoin version of the node it's running against. This is not the case yet for blockstream-electrs.

Yeah, we should def. not lean on proprietary optional features for this.

@tankyleo tankyleo Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Added docs on Config.anchor_channels_config, and a note in the pending changelog.
  • Added TODO + issue to drop 0FC chain source validation when support is ubiquitous.
  • From the x-bitcoin-version header, looks like mempool is running v30, I have not checked blockstream, I'll try to broadcast ephemeral dust against them tomorrow.

Comment thread src/lib.rs Outdated
.init_features;
let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx();
let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx()
|| init_features.requires_anchor_zero_fee_commitments();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, pre-existing, but it seems for init features we should be using supports_?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @febyeji had a similar question further above in this comment: #660 (comment)

Can we revisit these checks in a standalone PR ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we revisit these checks in a standalone PR ?

Well, if we touch it here we should at least not add incorrect code if we think it's incorrect. If you prefer a separate PR, feel free to open an base this one on top, but could also make it a prefactor commit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok will add a prefactor commit then

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added two commits below

@tankyleo

Copy link
Copy Markdown
Contributor Author

Let me know if I can squash

@tankyleo tankyleo requested a review from tnull June 25, 2026 15:30

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please squash!

Comment thread src/chain/bitcoind.rs Outdated
}

pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) {
fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the DRYing up a freestanding prefactor commit and then use it in a fixup to your code (to not touch unrelated preexisting code in your commits)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's the plan!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, see commit below

Comment thread src/chain/electrum.rs Outdated
let spawn_fut =
self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx));
let spawn_fut = self.runtime.spawn_blocking({
let tx = tx.clone();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this clone now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the log helpers further below take a &[Transaction] now, will rework things to remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done below

Comment thread src/chain/electrum.rs Outdated
let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect();

let spawn_fut = self.runtime.spawn_blocking({
let package = package.clone();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid cloning the whole package? It seems it's only required because we need the reference later for logging, but previously we intentionally avoided the allocations. Can we do the same still here and above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good will remove these clones thank you

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/liquidity/service/lsps2.rs Outdated
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
let spendable_amount_sats =
self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0);
let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude:

  1. /home/tnull/workspace/ldk-node-pr-660/src/liquidity/service/lsps2.rs:455: git diff --check upstream/main...HEAD fails because added lines contain trailing \r whitespace at lines 455, 456, and 459.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed all carriage returns across all rust files in a prefactor commit below.

Comment thread src/chain/esplora.rs
}

pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also want to note that this way of checking support for Electrum/Esplora is error prone as it will pass if the service runs bitcoind v26+ I believe, while we require v29+ in reality?

Comment thread src/tx_broadcaster.rs Outdated

const BCAST_PACKAGE_QUEUE_SIZE: usize = 50;

pub(crate) struct TransactionBroadcast(Vec<Transaction>);

@tnull tnull Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, this is now redudant/overlapping with the BroadcastPackage in #888. Not sure if you want to coordinate to use exactly the same type in both PRs ahead of time, or just have one rebase depending what ends up landing first? @tankyleo @jkczyz

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea happy to rebase myself in case that's needed no problem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to rename the type, BTW, if TransactionBroadcast is more accurate. I didn't put much thought on checking the naming.

@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from fcf54fd to 5eab461 Compare June 28, 2026 00:36
@tankyleo

Copy link
Copy Markdown
Contributor Author

I just added this last commit here "Tighten requirements to unset anchor channels config", if it looks good I'll adapt it to a prefactor commit to the "Check that the chain source supports 0FC channels" commit.

@tankyleo tankyleo requested a review from tnull June 29, 2026 05:12

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a rebase now that #888 landed. Sorry!

Feel free to squash, I'll do a full pass once it's rebased and hopefully we should be close then.

tankyleo added 15 commits June 29, 2026 17:33
We only do this for rust files, and leave bat files untouched.

Use git show --ignore-cr-at-eol to check that this commit has no other
edits.
`BroadcasterInterface::broadcast_transactions` requires that any passed
vector containing multiple transactions must be a single child together
with its parents. We will lean on this contract in upcoming commits, so
here we fix a case where we broke this contract.
In an upcoming commit, we will fix `check_sufficient_funds_for_channel`
to check that we have on-chain funds to cover the anchor reserve for an
additional anchor channel in the validation of outbound channel opens.

Before we do this, we stop using this function to check that any
splice-ins leave enough on-chain anchor reserves. This function keeps
an anchor reserve for an additional anchor channel on top of the
existing set of anchor channels, but after splice-ins, our anchor
reserve only needs to cover the existing set of anchor channels.
When we are preparing to open a channel to a peer, we should reserve
onchain funds for an anchor channel when the peer's init features
signals anchor channels as optional, as channel negotiation with such a
peer can result in an anchor channel.

Tests written with codex.
The patch adds support for the `broadcast_package` method added in
electrum protocol v1.6. Upcoming commits will require this patch to pass
CI.
The mempool/electrs docker image used in those tests only supports
submitpackage via the esplora interface, not the electrum interface.
We bump the Bitcoin Core version used in kotlin and python tests to
support ephemeral dust. This is required for 0FC channels.
Do this roundtrip at the same time we make a roundtrip to retrieve the
feerates to keep startup as fast as possible.
Implementations of `BroadcasterInterface` cannot assume any topological
ordering on the transactions received, so here we order the received
transactions before adding them to the broadcast queue. Any consumers of
the queue can now assume all transactions received to be topologically
sorted.

Codex wrote the tests.
These will be useful when we add support for broadcasting packages in an
upcoming commit.
We rely on the `BroadcasterInterface` contract whereby any
multi-transaction vector must be a single child and its parents, and
must be broadcasted together as a package using `submitpackage`.

In a prior commit, we added the guarantee that any packages received
from the broadcast queue are already topologically sorted, and hence
can be passed directly to the `submit_package` Bitcoin Core RPC.
We previously allowed users to unset the anchor channels config while
they still had anchor channels open or unresolved. This allowed our
users to drain their anchor reserves while still having anchor channels
open. This is particularly dangerous for 0FC channels, as these rely
entirely on anchor bumps to force-close the channel.

Here, we require that a user first close and resolve all their existing
anchor channels before unsetting their anchor channels config to disable
the opening of fresh anchor channels.

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment, otherwise LGTM! Unfortunately needs another tiny rebase by now due to minor conflicts in the test files.

Comment thread src/lib.rs
are still open or unresolved. You must close and resolve all anchor \
channels before disabling anchor channels."
);
return Err(Error::ChannelConfigUpdateFailed);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I really dislike the pattern of letting users configure something that we end up erroring on. If we think we need to be more strict now, we should probably find an API that doesn't allow the user to setup an 'unsafe' configuration in the first place.

In LDK we previously discussed making anchors mandatory. Should we rather trailblaze here, and simply make AnchorsChannelConfig non-optional?

If we do this, maybe we should make negotiating 0fc optional though, if only because of the chain source requirements, and we want to maintain backwards compat for a while?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All makes sense to me, will prepare the fixups.

we want to maintain backwards compat for a while

A clarification I thought ldk-node does not allow downgrades ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benthecarman does the above sound good to you ? First make anchors required in this release, then next release we turn on 0FC channels by default ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by anchors we just mean that they should always be available for negotiation, we will still negotiate down to legacy channels if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Goal: Merge

Development

Successfully merging this pull request may close these issues.

6 participants