A Look at LDK’s Dual-funded Channels Implementation
Dual-funded channels — or V2-established channels as referred to in the BOLT 2 Lightning Network specification — have been a long time coming. This protocol has already been adopted by the Eclair and Core Lightning (CLN) node implementations. We take a closer look at the progress and architecture of the dual-funded channel implementation in the Lightning Development Kit (LDK), and what makes this challenging.
#Relevant background on LDK’s architecture
You may be aware that the base LDK project — the lightning crate, its language bindings, and friends — is a library for plugging into the Lightning Network, handling all the intricacies of the specification and leaving a significant amount of flexibility for the developer integrating LDK into their application. Consequently, LDK must make more abstractions around interfaces that are not made by Lightning Network node implementations such as LND, CLN, and Eclair which act as standalone daemons out of the box.
One instance of such an abstraction is making few assumptions of the environment in which the application lives. It may be embedded, as is the case for Validating Lightning Signer (VLS), and would need to work in a no-std world. In other words, LDK does not expect an operating system.
#Interfacing with the world
LDK provides the interfaces (traits
in Rust) that developers can implement to provide essential functionality needed
for the operation of a Lightning Network node, such as persistence, signing, and networking. LDK also makes no
assumptions around the existence of a particular Bitcoin node implementation such as Bitcoin Core or btcd, so it
cannot rely on ‘ye old faithful and familiar Bitcoin Core RPCs to peek at what’s happening at the base layer.
All communication into and outside LDK’s very specific view of the Lightning Network protocol is via the shuffling
of bytes through a developer’s implementation of the required interfaces.
#Event Handling
One unique example of a trait in LDK that a developer would need to implement is the EventHandler
:
pub trait EventHandler {
// Required method
fn handle_event(&self, event: Event) -> Result<(), ReplayEvent>;
}
The single required method, handle_event()
, must be implemented to handle the various user-actionable Event
s
generated by LDK, of which there are many variants (currently for v0.1.1):
pub enum Event {
FundingGenerationReady {...},
FundingTxBroadcastSafe {...},
PaymentClaimable {...},
PaymentClaimed {...},rd
ConnectionNeeded {...},
InvoiceReceived {...},
PaymentSent {...},
PaymentFailed {...},
PaymentPathSuccessful {...},
PaymentPathFailed {...},
ProbeSuccessful {...},
ProbeFailed {...},
PendingHTLCsForwardable {...},
HTLCIntercepted {...},
SpendableOutputs {...},
PaymentForwarded {...},
ChannelPending {...},
ChannelReady {...},
ChannelClosed {...},
DiscardFunding {...},
OpenChannelRequest {...},
HTLCHandlingFailed {...},
BumpTransaction(BumpTransactionEvent),
OnionMessageIntercepted {...},
OnionMessagePeerConnected {...},
}
Specifically looking at the OpenChannelRequest
variant, if we do not specify the automatic acceptance of inbound
channels under certain conditions, we receive an event with all the relevant information about the proposed inbound
channel from a peer.
OpenChannelRequest {
temporary_channel_id: ChannelId,
counterparty_node_id: PublicKey,
funding_satoshis: u64,
channel_negotiation_type: InboundChannelFunds,
channel_type: ChannelTypeFeatures,
is_announced: bool,
params: ChannelParameters,
},
In particular, the channel_negotiation_type
field would specify whether this request is for an inbound dual-funded
channel, or the push_msats
value for an inbound V1 channel.
#Background tasks
LDK makes progress in state via user-called methods, and messages received from peers. There are still many tasks
that need to be completed in the background, not dependent on receiving peer messages or user method calls.
These include management of feerate estimates for our outbound channels, managing peer connectivity, expiring
outbound payments, and so forth. In order to make progress in this case, the ChannelManager::timer_tick_occurred()
method must be called every minute.
#The current implementation design and status of dual-funded channels in LDK
Although still in-progress, there are some design considerations we can discuss about LDK’s implementation of dual-funded channels.
#The interactive transaction constructor
Dual-funded and splicing both depend on the interactive transaction construction protocol, specified in BOLT 2. In LDK this is implemented as a type-safe state machine that is driven to a completely constructed funding transaction that is ready for interactive signing.
BOLT 2 also provides an example of a message exchange between the channel initiator and channel acceptor during interactive transaction construction:
+--------+ +--------+
| |--(1)- tx_add_input ---->| |
| |<-(2)- tx_complete ------| |
| |--(3)- tx_add_output --->| |
| |<-(4)- tx_complete ------| |
| |--(5)- tx_add_input ---->| |
| A |<-(6)- tx_add_input -----| B |
| |--(7)- tx_remove_output >| |
| |<-(8)- tx_add_output ----| |
| |--(9)- tx_complete ----->| |
| |<-(10) tx_complete ------| |
+--------+ +--------+
LDK encapsulates this state machine in the internal InteractiveTxConstructor
struct which is held by pending
dual-funded channels.
#Public APIs for dual-funded channels
In order to support dual-funded channels, we need to introduce some public methods to ChannelManager
for
creating and accepting dual-funded channels, and signing their funding transactions. Opening dual-funded
channels will use a new create_dual_funded_channel
method where a user will need to provide the funding
inputs to be added during interactive transaction construction. Similarly for accepting dual-funded channels
where the acceptor will be contributing, inputs would also need to be provided to that method.
A major difference comes up during signing the funding transaction. With V1 channels we would expect the user
to create and sign the funding transaction based on information we give to them such as the funding outpoint, etc.
In the case of dual-funded channels, the funding transaction is interactively constructed and we need to provide
the unsigned transaction to the user via an event so that they can sign the inputs they provided and provide those
witnesses back to ChannelManager
for that specific ChannelId
. For either case, LDK handles the broadcasting of
the funding transaction when it is safe to do so.
#The status of dual-funding in LDK
A lot of work has gone into internal refactoring in Channel
and ChannelManager
to pave the way for dual-funding
(and splicing) support. LDK — being a library by nature — made some of the dual-funding implementation details a
little tricky with some outstanding challenges, such as interactive RBF, still needing to be solved.
There is internal support for accepting dual-funded channels at the moment (since v0.1.0), but it is cfg-flagged at the moment as some quirks are ironed out. Full support for creating and accepting dual-funded channels — without RBF support — can be expected in LDK v0.2.0.