Skip to content

Zero-fee commitments support#660

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

Zero-fee commitments support#660
tankyleo wants to merge 25 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
/// `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
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.
@tankyleo tankyleo force-pushed the 25-10-0fc-channel-config branch from ad147c7 to b600609 Compare July 11, 2026 02:39
Comment thread tests/common/mod.rs
Comment on lines +386 to +388
let zero_fee_commitments = rand::random_bool(0.7);
println!("Negotiating zero-fee commitment channels: {}", zero_fee_commitments);
node_config.anchor_channels_config.enable_zero_fee_commitments = zero_fee_commitments;

@tankyleo tankyleo Jul 13, 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.

@tnull Note this currently means running the test suite locally now requires something similar to

ELECTRS_EXE=/data/code/blockstream-electrs/target/debug/electrs cargo test

where blockstream-electrs is a clone of my own fork of the repo that adds submitpackage support, see the CI scripts for further details. It can also be a pointer to the current master of blockstream-electrs IIUC.

Are we good with this, or do we want to find a better solution here ? blockstream-electrs just added electrum support for submitpackage, so we can at least remove the current CI pointer to my own fork of blockstream-electrs.

Then we have to figure out a path forward for the existing reliance on binaries served directly from Rcasatta/electrsd.

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, yeah, that's really not great. It's kind of important to keep local testing working, also without installing and messing with an env variable. If we really need to do this, we at the very least need to create a new script that wraps cargo test and does the necessary pre-requisites. But maybe it actually means we need to push everybody to get upgraded and wait with landing this...

blockstream-electrs just added electrum support for submitpackage, so we can at least remove the current CI pointer to my own fork of blockstream-electrs.

Well, see Blockstream/electrs#172 (comment): it seems to be broken currently, i.e., we can't use it.

Then we have to figure out a path forward for the existing reliance on binaries served directly from Rcasatta/electrsd.

Well, mostly we need to get electrsd upgraded. See the discussions over at rust-bitcoin/corepc#655

@tankyleo tankyleo Jul 14, 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.

One potential path forward let me know what you think: just drop the last commit. This last commit is what actually turns on 0FC channels in ldk-node, prior to that commit the enable_zero_fee_commitments flag does not do much.

Quite confident CI and regular cargo test would all pass.

Otherwise, yes happy to pause work on this PR and we can resume once electrsd is upgraded / we have found a good path forward there.

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 and I would also drop the "Use a patched blockstream-electrs in CI" commit since we would no longer test 0FC channels in CI.

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 potential path forward let me know what you think: just drop the last commit. This last commit is what actually turns on 0FC channels in ldk-node, prior to that commit the enable_zero_fee_commitments flag does not do much.

How about we just make the 0fc test cases a dedicated integration test file / workflow gated behind cfg(0fc_tests) and only there install the custom electrs? (hopefully we can remove the cfg-gate soon).

Then we could at least continue to run any non-0fc tests locally, and still get coverage for 0fc in CI?

@tankyleo tankyleo requested a review from tnull July 13, 2026 07:10

@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.

Mostly looks good. When I started out reviewing I expected to land this and have comments addressed as a small follow-up, but it seems there are still a few things left that we need to clarify before we can move forward (esp. if/how to deal with the unupgraded dev dependencies, as breaking local testing is not great).

Comment thread src/wallet/mod.rs

if amount_msat == Some(0) && fee_paid_msat == Some(0) {
if (amount_msat == Some(0) && fee_paid_msat == Some(0))
|| matches!(tx_type, TransactionType::UnilateralClose { .. })

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, these don't directly interact with the onchain wallet, but wouldn't it still make sense to record them as otherwise users would have a 'gap' between channel funding and the resulting Sweep transaction? If we keep them that could allow them to track the funds end-to-end, on each step?

More generally, is this a required prefactor for 0fc? Otherwise it would be better to move this change to a separate PR and discuss it separately, esp. given this PR is already pretty big as-is.

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 opened this PR, we can continue the discussion there #979

Comment thread src/lib.rs Outdated
Ok(())
}

fn check_sufficient_funds_for_splice_in(&self, amount_sats: u64) -> Result<(), Error> {

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.

We should probably DRY this up with check_sufficient_funds_for_channel. The splice variant could be just a bool or similar on the same helper method rather than duplicating 2/3 of 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/wallet/mod.rs Outdated
.into_iter()
.filter_map(|txid| {
let tx = locked_wallet.tx_details(txid).map(|d| (*d.tx).clone())?;
self.broadcaster.broadcast_unclassified_transactions(vec![tx]);

@tnull tnull Jul 13, 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.

By now we clone and re-allocate vectors ~3 times or so until transactions are actually broadcast. As a follow-up / extra commit, we should try to optimize this again to avoid as many as possible.

Something like:

diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs
index bba4e083..ad2ae789 100644
--- a/src/tx_broadcaster.rs
+++ b/src/tx_broadcaster.rs
@@ -33,7 +33,7 @@ impl BroadcastPackage {
        }

        /// Builds a package for wallet-originated broadcasts that have no LDK classification.
-       fn unclassified(txs: Vec<Transaction>) -> Self {
+       fn unclassified(txs: impl IntoIterator<Item = Transaction>) -> Self {
                Self(txs.into_iter().map(|tx| (tx, None)).collect())
        }

@@ -150,7 +150,9 @@ where
                Ok(package)
        }

-       pub(crate) fn broadcast_unclassified_transactions(&self, txs: Vec<Transaction>) {
+       pub(crate) fn broadcast_unclassified_transactions(
+               &self, txs: impl IntoIterator<Item = Transaction>,
+       ) {
                self.queue_sender.try_send(BroadcastPackage::unclassified(txs)).unwrap_or_else(|e| {
                        log_error!(self.logger, "Failed to broadcast transactions: {}", e);
                });
diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs
index 42fb3bcc..788c4663 100644
--- a/src/wallet/mod.rs
+++ b/src/wallet/mod.rs
@@ -329,8 +329,10 @@ impl Wallet {
                                        let count: usize = unconfirmed_outbound_txids
                                                .into_iter()
                                                .filter_map(|txid| {
-                                                       let tx = locked_wallet.tx_details(txid).map(|d| (*d.tx).clone())?;
-                                                       self.broadcaster.broadcast_unclassified_transactions(vec![tx]);
+                                                       let tx = locked_wallet.tx_details(txid).map(|d| d.tx)?;
+                                                       self.broadcaster.broadcast_unclassified_transactions(
+                                                               std::iter::once(tx.as_ref()).cloned(),
+                                                       );
                                                        Some(())
                                                })
                                                .count();
@@ -878,7 +880,7 @@ impl Wallet {
                };

                let txid = tx.compute_txid();
-               self.broadcaster.broadcast_unclassified_transactions(vec![tx]);
+               self.broadcaster.broadcast_unclassified_transactions([tx]);

                match send_amount {
                        OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => {
@@ -1760,7 +1762,7 @@ impl Wallet {
                self.runtime
                        .block_on(self.pending_payment_store.insert_or_update(pending_payment_store))?;

-               self.broadcaster.broadcast_unclassified_transactions(vec![fee_bumped_tx]);
+               self.broadcaster.broadcast_unclassified_transactions([fee_bumped_tx]);

                log_info!(self.logger, "RBF successful: replaced {} with {}", txid, new_txid);

@tankyleo tankyleo Jul 14, 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.

Thank you made a few cleanups below, see "Remove single transaction vecs in wallet broadcast path" and "f: remove a collect call in into_sorted_transactions"

Comment thread tests/common/mod.rs
Comment on lines +386 to +388
let zero_fee_commitments = rand::random_bool(0.7);
println!("Negotiating zero-fee commitment channels: {}", zero_fee_commitments);
node_config.anchor_channels_config.enable_zero_fee_commitments = 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, yeah, that's really not great. It's kind of important to keep local testing working, also without installing and messing with an env variable. If we really need to do this, we at the very least need to create a new script that wraps cargo test and does the necessary pre-requisites. But maybe it actually means we need to push everybody to get upgraded and wait with landing this...

blockstream-electrs just added electrum support for submitpackage, so we can at least remove the current CI pointer to my own fork of blockstream-electrs.

Well, see Blockstream/electrs#172 (comment): it seems to be broken currently, i.e., we can't use it.

Then we have to figure out a path forward for the existing reliance on binaries served directly from Rcasatta/electrsd.

Well, mostly we need to get electrsd upgraded. See the discussions over at rust-bitcoin/corepc#655

Comment thread tests/common/logging.rs Outdated
}

#[cfg(feature = "uniffi")]
impl LogWriter for MockLogFacadeLogger {

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.

Not sure I understand why we need this 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.

We now use MockLogFacadeLogger in lsps2_rejects_jit_channel_without_anchor_reserve which we added in this commit.

Otherwise CI fails, as codex describes below:

_ The CI step is Test with UniFFI support in .github/workflows/rust.yml:96:

  - name: Test with UniFFI support on Rust ${{ matrix.toolchain }}
    if: "matrix.platform != 'windows-latest' && matrix.build-uniffi"
    run: |
      RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --features uniffi

  I reproduced the narrower compile-only version by checking out c324672 in /tmp, removing just that #[cfg(feature = "uniffi")] impl LogWriter for MockLogFacadeLogger, and running:

  RUSTFLAGS="--cfg no_download --cfg cycle_tests" \
    cargo test --features uniffi --test integration_tests_rust --no-run

  It fails at the new LSPS2 test:

  error[E0277]: the trait bound `MockLogFacadeLogger: ldk_node::logger::LogWriter` is not satisfied
      --> tests/integration_tests_rust.rs:3039:36
       |
  3039 |     service_builder.set_custom_logger(service_logger.clone());
       |                                       ^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound

  So the failing build step was the UniFFI test build, not plain cargo build --features uniffi.

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.

See the new approach below, I remove the log assertions so remove this impl block here.

Comment thread scripts/build_electrs.sh

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.

Could you expand on why this needs to be a separate script now? Why not keep using the old script that sets up both bitcoind and electrs?

@tankyleo tankyleo Jul 14, 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.

Codex:

 On your main question: a separate build_electrs.sh file is not strictly necessary, but a separate electrs execution path is. The old combined script plus this condition:

  cache-bitcoind miss || cache-electrs miss

  would always build/download both artifacts when either cache missed. The current split fixes that: download_bitcoind.sh only runs on a bitcoind miss, and build_electrs.sh only runs on an electrs miss.

  So I_d keep the logical split. If you dislike the extra file, the equivalent would be one script with subcommands, e.g. scripts/setup_ci_binary.sh bitcoind and scripts/setup_ci_binary.sh electrs. I would not go back to one
  combined script invoked by the OR condition.

This matches my original intention when I first created this standalone script for the expensive electrs build step.

It looks like we want to avoid building the binary ourselves anyway for easier local development, so I will likely be able to consolidate things again once a binary that supports submitpackage on electrum is served somewhere.

@tnull tnull Jul 14, 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.

This matches my original intention when I first created this standalone script for the expensive electrs build step.

Okay, yeah, not too strong of an opinion.

It looks like we want to avoid building the binary ourselves anyway for easier local development, so I will likely be able to consolidate things again once a binary that supports submitpackage on electrum is served somewhere.

Well, given how close this PR is it would be nice to land it for 0.8 still, and not wait indefinitely. Hmm, given that the Forgejo migration is already around the corner, further CI changes will be necessary anyways soon. Maybe we could at least provide a convenience wrapper script here, that sets up bitcoind/electrsd and runs the tests? Or even better just do #660 (comment), i.e., leave the current test setup as is and only test 0fc in a separate new file and workflow, so we don't require the custom electrs for all local tests?

Comment thread src/config.rs
/// confirm the commitment from the anchor reserve at the time of broadcast. If set, your
/// chain source *must* support the `submitpackage` Bitcoin Core RPC, and relay TRUC, P2A,
/// and ephemeral dust.
/// See [BOLT 3] for more technical details.

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 link is broken.

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/config.rs Outdated
/// might not suffice to successfully spend the Anchor output and have the HTLC transactions
/// confirmed on-chain, i.e., you may want to adjust this value accordingly.
pub per_channel_reserve_sats: u64,
/// In addition to `option_anchors_zero_fee_htlc_tx`, we will also attempt to negotiate

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 rewrite these docs to explain from the users PoV what this will gain them and what the risks are if they enable it.

@tankyleo tankyleo Jul 14, 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.

Did you have any specific upsides and downsides in mind? Seems to me the existing doc shows the main upside and downside (along with the submitpackage requirement), I expanded further those two points in a fixup below, but I didn't do a full rewrite.

Comment thread src/lib.rs Outdated
tokio::try_join!(
chain_source.update_fee_rate_estimates(),
chain_source.validate_zero_fee_commitments_support_if_required(
any_current_0fc_channels

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.

Let's move this OR into a bool first.

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/lib.rs Outdated
.channel_type
.as_ref()
.map_or(false, |c| c.requires_anchor_zero_fee_commitments())
}) || self.chain_monitor.list_monitors().into_iter().any(|channel_id| {

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.

Ugh, that's pretty ugly. Please somewhat clean this up, maybe by breaking into separate bools if required.

Also, why do we need to list the monitors separately at all? Wouldn't listing channels suffice?

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, why do we need to list the monitors separately at all? Wouldn't listing channels suffice?

Used codex to research this, See https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/src/commit/506cb91f2e0fb87906188b79777bcf42595d3623/lightning/src/ln/channelmanager.rs#L4574

At this point we remove the channel from list_channels before the force-close commitment transaction has confirmed, and before the channel monitor makes the first broadcast of the commitment transaction. So it is very possible to require submitpackage support when a channel shows up only in the monitor.

I also asked codex if we could look only at the monitor, and the answer was no:

_ can you remind me again why only looking at the monitor is not enough ? We should also look at the channel manager ?


_ Only looking at ChainMonitor::list_monitors() misses channels that exist in ChannelManager but do not yet have a monitor.

  That can happen during channel setup. ChannelManager::list_channels() returns channels from peer_state.channel_by_id, and ChannelDetails.channel_type can already be known after open/accept negotiation:

  /home/ubuntu/.cargo/git/checkouts/rust-lightning-e47a539aa5104a91/506cb91/lightning/src/ln/channel_state.rs:308

  /// None until negotiation completes and the channel type is finalized.
  pub channel_type: Option<ChannelTypeFeatures>,

  But the ChannelMonitor is created later, when the channel reaches the initial commitment/funding-signed phase. For example, LDK only calls watch_channel(...) once it has a monitor:

  /home/ubuntu/.cargo/git/checkouts/rust-lightning-e47a539aa5104a91/506cb91/lightning/src/ln/channelmanager.rs:11983

  self.chain_monitor
      .watch_channel(funded_chan.context.channel_id(), monitor)

  So the two sources cover different lifecycle windows:

  - ChannelManager: catches negotiated/current channels before a monitor exists.
  - ChainMonitor: catches funded/closed/force-closed channels after the manager may have dropped them.

  For the startup 0FC chain-source validation, relying only on monitors would miss a negotiated 0FC channel in the pre-monitor phase. Relying only on the manager would miss force-closed channels still resolving on-chain.
  That_s why the current OR over both is the conservative check.

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.

Ugh, that's pretty ugly. Please somewhat clean this up, maybe by breaking into separate bools if required.

I broke this up into separate bools below

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.

Okay, but it's a bit unfortunate as we retain old monitors for 4032 blocks before they are archived. So we at the very least need to filter out the fully resolved monitors as they are just waiting to be archived.

However, that also raises the question if not-fully-resolved monitors need to be technically reflected in the reserve calculations? It seems that might be a gap right now?

tankyleo added 22 commits July 14, 2026 05:11
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.
We previously allowed users to disable anchor channels and drain their
anchor reserve while still having anchor channels open or pending
resolution. This was acceptable for keyed anchor channels, as the
commitment transaction therein still contained some fees, and had some
chance of getting mined into a block without any anchor bumps.

In upcoming commits, we will add support for 0FC channels, and their
commitment transactions have zero fees and depend entirely on the anchor
reserve to reach miners and get confirmed in a block. It is thus
dangerous to disable anchor channels and drain the reserve after 0FC
channels have been opened. Therefore, we make `AnchorChannelsConfig`
required, and prevent this case from ever happening.
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.
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.
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.

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 avoid broadcasting non-TRUC parents-child packages via
`submitpackage` for now to avoid adding a requirement to support
`submitpackage` for users that don't enable zero-fee commitment
channels. We will do so once support for `submitpackage` is more
ubiquitous.
@tankyleo

Copy link
Copy Markdown
Contributor Author

I believe I've addressed / responded all your comments thank you again. Feel free to remove the request if you'd like to resume this PR once we've made progress on electrsd. We also have an alternative path forward that I describe in a thread above.

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