Skip to content

Allow specifying a fee rate for channel-open funding transactions - #1099

Merged
tnull merged 3 commits into
lightningdevkit:mainfrom
ZeusLN:channel-open-fee-rate-upstream
Oct 14, 2026
Merged

tnull merged 3 commits into
lightningdevkit:mainfrom
ZeusLN:channel-open-fee-rate-upstream

Conversation

@kaloudis

Copy link
Copy Markdown
Contributor

Motivation

Wallet UIs commonly collect a sat/vB fee rate in their channel-open flow, but LDK Node offers no way to apply it: the funding transaction is always built at the internal estimator's ChannelFunding rate. #176 added exactly this kind of override for the on-chain send APIs; this PR extends the same pattern to channel opens.

Surveying ldk-node integrators: Zeus collects an open-channel fee rate in its UI and had to carry this as a downstream fork patch (ZeusLN#6, adapted here to current main), and Alby Hub and Bitkit currently have no way to offer the option at all through ldk-node's API.

Changes

All Node::open_channel variants (open_channel, open_announced_channel, the _with_all and _0reserve variants) gain a trailing Option<FeeRate> (FeeRate? in bindings):

  • open_channel_inner stashes the rate in a new pending_funding_fee_rates: Mutex<HashMap<u128, FeeRate>> keyed by user_channel_id once create_channel succeeds
  • the FundingGenerationReady handler removes the entry and passes it to Wallet::create_funding_transaction, which now takes Option<FeeRate> and falls back to the estimator when None (same shape as send_to_address)
  • for FundingAmount::Max the override also feeds get_max_funding_amount, so the maximum amount is computed at the rate the funding transaction will actually pay
  • ChannelClosed clears any still-pending entry, so state stashed for a channel that is rejected or dies before funding does not leak

When the parameter is unset, behavior is byte-for-byte unchanged.

This is a breaking signature change on the six open methods; a Feature and API updates CHANGELOG entry is included, and all internal call sites (tests, doctest) are updated.

Testing

  • cargo check, cargo check --features uniffi, and cargo check --tests pass
  • cargo test --lib (196 tests) and cargo test --doc pass
  • cargo fmt --all -- --check clean

An earlier version of this change (against the 0.7 API) is what Zeus ships; it has unit coverage on the wallet side and device testing pending there.

Disclosure

This change was developed with the assistance of an AI coding tool (Claude Code), per the repository's contribution guidelines.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 14, 2026 •

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.

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

Hmm, I'm honestly not convinced we should do this as it kind of introduces additional footguns that might be more easily to hit in comparison to the onchain send cases.

Do you really expose manual fee rate setting to the user? Can you explain why a user wants to set a specific manual fee rate override? Would having more general fee estimator profiles would make more sense (allowing to specify 'economical' vs. 'fast confirmation' or similar)?

@kaloudis

Copy link
Copy Markdown
Contributor Author

Hmm, I'm honestly not convinced we should do this as it kind of introduces additional footguns that might be more easily to hit in comparison to the onchain send cases.

Do you really expose manual fee rate setting to the user? Can you explain why a user wants to set a specific manual fee rate override? Would having more general fee estimator profiles would make more sense (allowing to specify 'economical' vs. 'fast confirmation' or similar)?

Yes, we've supported manual fee rates for on-chain sends and channel operations since the early days of ZEUS. We've defaulted to using recommendations from our Mempool.space/Esplora instances but some users want more granular controls, especially in higher fee rate environments.

@tnull

tnull commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Yes, we've supported manual fee rates for on-chain sends and channel operations since the early days of ZEUS. We've defaulted to using recommendations from our Mempool.space/Esplora instances but some users want more granular controls, especially in higher fee rate environments.

Alright, honestly don't quite buy the argument for mobile (or rather think that if it just were for mobile we could find a better API than raw fee rates), but I do buy it for LDK Server users. So fine by me to move forward with this.

CI is currently failing though, could you also adjus the Python/Kotlin tests for the new API?

@kaloudis
kaloudis force-pushed the channel-open-fee-rate-upstream branch from db51d21 to f1128f6 Compare September 17, 2026 17:21
@kaloudis

kaloudis commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Yes, we've supported manual fee rates for on-chain sends and channel operations since the early days of ZEUS. We've defaulted to using recommendations from our Mempool.space/Esplora instances but some users want more granular controls, especially in higher fee rate environments.

Alright, honestly don't quite buy the argument for mobile (or rather think that if it just were for mobile we could find a better API than raw fee rates), but I do buy it for LDK Server users. So fine by me to move forward with this.

CI is currently failing though, could you also adjus the Python/Kotlin tests for the new API?

Tests updated

Comment thread src/lib.rs Outdated
);
if let Some(fee_rate) = fee_rate {
self.pending_funding_fee_rates
.lock()

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.

AI:

[P2] Register the fee override before creating the channel — /home/tnull/workspace/ldk-node-review-pr-1099-T3mKz4/src/lib.rs:1475. create_channel releases its locks and queues the open message before this insertion. If the caller pauses—such as during the preceding log write—the background processor can handle FundingGenerationReady first and silently use the estimator’s rate. This can overpay fees or fail a _with_all opening calculated at the requested rate. Insert the override before calling LDK, and remove it if creation fails.

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.

Good catch. The override is now inserted into pending_funding_fee_rates before create_channel or create_channel_to_trusted_peer_0reserve is called. user_channel_id is already generated at that point. On Err the entry is removed. If add_peer fails after a successful create_channel the entry stays, since the channel is live and ChannelClosed cleans it up. Fixed in 1669afc.

Comment thread src/lib.rs
//! let node_id = PublicKey::from_str("NODE_ID").unwrap();
//! let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap();
//! node.open_channel(node_id, node_addr, 10000, None, None).unwrap();
//! node.open_channel(node_id, node_addr, 10000, None, None, None).unwrap();

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 need to also update the example in README.md

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.

Updated in 090dc4f

Comment thread src/lib.rs Outdated
pub fn open_channel(
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
fee_rate: Option<FfiFeeRate>,

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.

nit: Can we move this (everywhere) after push_to_counterparty_msat, so that channel_config remains the last argument?

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 in 8c67453. fee_rate now comes right after push_to_counterparty_msat in all six methods and the UDL, so channel_config stays last. Also changed the CHANGELOG entry, which called it "trailing".

Comment thread src/lib.rs Outdated
#[cfg(feature = "uniffi")]
type FfiFeeRate = Arc<bitcoin::FeeRate>;

macro_rules! maybe_map_fee_rate_opt {

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 move this macro to ffi/mod.rs and preferably DRY up with the one in payment/onchain.rs.

Would also be great to make it a method rather than a macro.

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 in 39a9570. It's now a maybe_map_fee_rate_opt() function in ffi/mod.rs, with uniffi/non-uniffi variants like maybe_deref/maybe_wrap, plus a shared FfiFeeRate alias. Both the lib.rs macro and the one in payment/onchain.rs are removed.

@kaloudis
kaloudis force-pushed the channel-open-fee-rate-upstream branch 2 times, most recently from a44ad44 to e1b7be0 Compare October 5, 2026 21:14
@kaloudis

kaloudis commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

@tnull great feedback, thank you. All fixes have separate commits. Let me know if you'd like to squash before merge.

@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 fixups. Feel free to pull out the helper moves into a freestanding prefactor commit.

Comment thread src/lib.rs Outdated
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount,
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
announce_for_forwarding: bool, disable_counterparty_reserve: bool,
fee_rate: Option<bitcoin::FeeRate>,

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.

nit: Please make this consistent and move it to before channel_config.

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 in 56ad543. fee_rate now comes before channel_config in open_channel_inner and all of its call sites.

@tnull

tnull commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

@kaloudis Please feel free to re-request review once you squashed.

Replace the `maybe_map_fee_rate_opt!` macro and `FeeRate` alias in
`payment/onchain.rs` with a `maybe_map_fee_rate_opt` function and a
shared `FfiFeeRate` alias in `ffi/mod.rs`, next to the other
`uniffi`/non-`uniffi` conversion helpers, so they can be reused outside
of `OnchainPayment`.
Add an optional `fee_rate` parameter to all `open_channel` variants,
ahead of `channel_config`. When set, it is used for the funding
transaction and for computing the maximum funding amount of the
`_with_all` variants instead of the `ChannelFunding` estimate.

The override is registered in `pending_funding_fee_rates` before
calling `create_channel`, so it is in place when
`FundingGenerationReady` is handled, and removed again if channel
creation fails. Pending overrides are cleared when channels close.

Update the README, docs, and Rust, Python, and Kotlin call sites for
the new parameter.
Open one channel with an exact amount and one via
`open_channel_with_all` at 25 sat/vB and check the funding transaction
pays that rate. The `_with_all` case also covers the maximum amount
being computed at the overridden rate.
@kaloudis
kaloudis force-pushed the channel-open-fee-rate-upstream branch from e1b7be0 to 11454c2 Compare October 13, 2026 14:22
@kaloudis
kaloudis requested a review from tnull October 13, 2026 14:28

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

Thanks! I think as a follow up we should now include the fees in the anchor reserve checks (cf. #1166), but landing this.

@tnull
tnull merged commit 60cc83f into lightningdevkit:main Oct 14, 2026
14 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants