Skip to main content

lexe_common/ln/
channel.rs

1use std::{
2    fmt::{self, Debug, Display},
3    str::FromStr,
4};
5
6use anyhow::Context;
7use lexe_byte_array::ByteArray;
8use lexe_crypto::rng::{RngCore, RngExt};
9use lexe_serde::hexstr_or_bytes;
10use lexe_sha256::sha256;
11use lexe_std::Apply;
12use lightning::{
13    chain::transaction::OutPoint,
14    ln::{channel_state::ChannelDetails, types::ChannelId},
15};
16#[cfg(any(test, feature = "test-utils"))]
17use proptest_derive::Arbitrary;
18use ref_cast::RefCast;
19use rust_decimal::Decimal;
20use rust_decimal_macros::dec;
21use serde::{Deserialize, Serialize};
22use serde_with::{DeserializeFromStr, SerializeDisplay};
23
24use crate::{
25    api::user::{NodePk, Scid},
26    ln::{amount::Amount, hashes::Txid},
27};
28
29/// A newtype for [`lightning::ln::types::ChannelId`].
30#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
31#[derive(Copy, Clone, Eq, PartialEq, Hash, RefCast, Serialize, Deserialize)]
32#[repr(transparent)]
33pub struct LxChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
34
35lexe_byte_array::impl_byte_array!(LxChannelId, 32);
36lexe_byte_array::impl_fromstr_fromhex!(LxChannelId, 32);
37lexe_byte_array::impl_debug_display_as_hex!(LxChannelId);
38
39impl From<ChannelId> for LxChannelId {
40    fn from(cid: ChannelId) -> Self {
41        Self(cid.0)
42    }
43}
44impl From<LxChannelId> for ChannelId {
45    fn from(cid: LxChannelId) -> Self {
46        Self(cid.0)
47    }
48}
49
50/// See: [`lightning::ln::channel_state::ChannelDetails::user_channel_id`]
51///
52/// The user channel id lets us consistently identify a channel through its
53/// whole lifecycle.
54///
55/// The main issue is that we don't know the [`LxChannelId`] until we've
56/// actually talked to the remote node and agreed to open a channel. The second
57/// issue is that we can't easily observe and correlate any errors from channel
58/// negotiation beyond some basic checks before we send any messages.
59#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
60#[derive(Copy, Clone, Eq, PartialEq, Hash, RefCast, Serialize, Deserialize)]
61#[repr(transparent)]
62pub struct LxUserChannelId(#[serde(with = "hexstr_or_bytes")] pub [u8; 16]);
63
64impl LxUserChannelId {
65    #[inline]
66    pub fn to_u128(self) -> u128 {
67        u128::from_le_bytes(self.0)
68    }
69
70    pub fn from_rng<R: RngCore>(rng: &mut R) -> Self {
71        Self(rng.gen_bytes())
72    }
73
74    pub fn derive_temporary_channel_id(&self) -> LxChannelId {
75        LxChannelId(sha256::digest(&self.0).to_array())
76    }
77}
78
79lexe_byte_array::impl_byte_array!(LxUserChannelId, 16);
80lexe_byte_array::impl_fromstr_fromhex!(LxUserChannelId, 16);
81lexe_byte_array::impl_debug_display_as_hex!(LxUserChannelId);
82
83impl From<u128> for LxUserChannelId {
84    fn from(value: u128) -> Self {
85        Self(value.to_le_bytes())
86    }
87}
88
89impl From<LxUserChannelId> for u128 {
90    fn from(value: LxUserChannelId) -> Self {
91        value.to_u128()
92    }
93}
94
95/// A newtype for LDK's [`ChannelDetails`] which implements the [`serde`]
96/// traits, flattens nested structure, and contains only the fields Lexe needs.
97#[derive(Debug, Serialize, Deserialize)]
98pub struct LxChannelDetails {
99    // --- Basic info --- //
100    pub channel_id: LxChannelId,
101    pub user_channel_id: LxUserChannelId,
102    /// The position of the funding transaction in the chain.
103    /// - Used as a short identifier in many places.
104    /// - [`None`] if the funding tx hasn't been confirmed.
105    /// - NOTE: If `inbound_scid_alias` is present, it must be used for
106    ///   invoices and inbound payments instead of this.
107    // Introduced in node-v0.6.21, lsp-v0.6.37
108    pub scid: Option<Scid>,
109    /// The result of [`ChannelDetails::get_inbound_payment_scid`].
110    /// NOTE: Use this for inbound payments and route hints instead of
111    /// [`Self::scid`]. See [`ChannelDetails::inbound_scid_alias`] for details.
112    // Introduced in node-v0.6.21, lsp-v0.6.37
113    pub inbound_payment_scid: Option<Scid>,
114    /// The result of [`ChannelDetails::get_outbound_payment_scid`].
115    /// NOTE: This should be used in `Route`s to describe the first hop and
116    /// when we send or forward a payment outbound over this channel.
117    /// See [`ChannelDetails::outbound_scid_alias`] for details.
118    // Introduced in node-v0.6.21, lsp-v0.6.37
119    pub outbound_payment_scid: Option<Scid>,
120    pub funding_txo: Option<LxOutPoint>,
121    // Introduced in node-v0.6.16, lsp-v0.6.32
122    pub counterparty_alias: Option<String>,
123    pub counterparty_node_id: NodePk,
124    pub channel_value: Amount,
125    /// The portion of our balance that our counterparty forces us to keep in
126    /// the channel so they can punish us if we try to cheat. Unspendable.
127    pub punishment_reserve: Amount,
128    /// The number of blocks we'll need to wait to claim our funds if we
129    /// initiate a channel close. Is [`None`] if the channel is outbound and
130    /// hasn't yet been accepted by our counterparty.
131    pub force_close_spend_delay: Option<u16>,
132    // This field was `is_public` prior to LDK v0.0.124
133    pub is_announced: bool,
134    pub is_outbound: bool,
135    /// (1) channel has been confirmed
136    /// (2) `channel_ready` messages have been exchanged
137    /// (3) channel is not currently being shut down
138    pub is_ready: bool,
139    /// (1) `is_ready`
140    /// (2) we are (p2p) connected to our counterparty
141    pub is_usable: bool,
142
143    // --- Our balance --- //
144    /// Our total balance. "The amount we would get if we close the channel*"
145    /// *if all pending inbound HTLCs failed and on-chain fees were 0
146    ///
147    /// Use this for displaying our "current funds".
148    pub our_balance: Amount,
149    /// Is: `balance - punishment_reserve - pending_outbound_htlcs`
150    ///
151    /// Use this as an approximate measurement of liquidity, e.g. in graphics.
152    pub outbound_capacity: Amount,
153    /// Roughly: `outbound_capacity`, but accounting for all additional
154    /// protocol limits like commitment tx fees, dust limits, and
155    /// counterparty constraints.
156    ///
157    /// Use this for routing, including determining the maximum size of the
158    /// next individual Lightning payment sent over this channel, or
159    /// determining how much can be sent in this channel's shard of a
160    /// multi-path payment.
161    pub next_outbound_htlc_limit: Amount,
162
163    // --- Their balance --- //
164    pub their_balance: Amount,
165    /// Approximately how much inbound capacity is available to us.
166    ///
167    /// Due to in-flight HTLCs, feerates, dust limits, etc... we cannot
168    /// receive exactly this value (likely a 1k-2k sats lower).
169    pub inbound_capacity: Amount,
170
171    // --- Fees and CLTV --- //
172    // These values may change at runtime.
173    /// Our base fee for payments forwarded outbound over this channel.
174    pub our_base_fee: Amount,
175    /// Our proportional fee for payments forwarded outbound over this channel.
176    /// Represented as a decimal (e.g. a value of `0.01` means 1%)
177    pub our_prop_fee: Decimal,
178    /// The minimum difference in `cltv_expiry` that we enforce for HTLCs
179    /// forwarded outbound over this channel.
180    pub our_cltv_expiry_delta: u16,
181    /// Their base fee for payments forwarded inbound over this channel.
182    pub their_base_fee: Option<Amount>,
183    /// Their proportional fee for payments forwarded inbound over this
184    /// channel. Represented as a decimal (e.g. a value of `0.01` means 1%)
185    pub their_prop_fee: Option<Decimal>,
186    /// The minimum difference in `cltv_expiry` our counterparty enforces for
187    /// HTLCs forwarded inbound over this channel.
188    pub their_cltv_expiry_delta: Option<u16>,
189
190    // --- HTLC limits --- //
191    /// The smallest inbound HTLC we will accept. Generally determined by our
192    /// [`ChannelHandshakeConfig::our_htlc_minimum_msat`](lightning::util::config::ChannelHandshakeConfig).
193    pub inbound_htlc_minimum: Amount,
194    /// The largest inbound HTLC we will accept. This is bounded above by
195    /// [`Self::channel_value`] (NOT [`Self::inbound_capacity`]).
196    pub inbound_htlc_maximum: Option<Amount>,
197    /// The smallest outbound HTLC our counterparty will accept. Assuming the
198    /// counterparty is a Lexe user or Lexe's LSP, this is determined by their
199    /// [`ChannelHandshakeConfig::our_htlc_minimum_msat`](lightning::util::config::ChannelHandshakeConfig).
200    pub outbound_htlc_minimum: Option<Amount>,
201    /// The largest outbound HTLC our counterparty will accept. Assuming the
202    /// counterparty is a Lexe user or Lexe's LSP, this appears to be bounded
203    /// above by [`Self::channel_value`] (NOT [`Self::outbound_capacity`]).
204    pub outbound_htlc_maximum: Option<Amount>,
205
206    // --- Features of interest that our counterparty supports --- //
207    // NOTE: In order to use these features, we must enable them as well.
208    pub cpty_supports_basic_mpp: bool,
209    pub cpty_supports_onion_messages: bool,
210    pub cpty_supports_wumbo: bool,
211    pub cpty_supports_zero_conf: bool,
212}
213
214impl LxChannelDetails {
215    /// Construct a [`LxChannelDetails`] from a LDK [`ChannelDetails`] and
216    /// other info.
217    ///
218    /// - The balance should be from [`ChannelMonitor::get_claimable_balances`];
219    ///   not to be confused with [`ChainMonitor::get_claimable_balances`].
220    ///
221    /// [`ChannelMonitor::get_claimable_balances`]: lightning::chain::channelmonitor::ChannelMonitor::get_claimable_balances
222    /// [`ChainMonitor::get_claimable_balances`]: lightning::chain::chainmonitor::ChainMonitor::get_claimable_balances
223    pub fn from_ldk(
224        details: ChannelDetails,
225        our_balance: Amount,
226        counterparty_alias: Option<String>,
227    ) -> anyhow::Result<Self> {
228        let inbound_payment_scid = details.get_inbound_payment_scid().map(Scid);
229        let outbound_payment_scid =
230            details.get_outbound_payment_scid().map(Scid);
231
232        // This destructuring makes clear which fields we *aren't* using,
233        // in case we want to include more fields in the future.
234        let ChannelDetails {
235            channel_id,
236            counterparty,
237            funding_txo,
238            channel_type: _,
239            short_channel_id,
240            outbound_scid_alias: _,
241            inbound_scid_alias: _,
242            channel_value_satoshis,
243            unspendable_punishment_reserve,
244            user_channel_id,
245            feerate_sat_per_1000_weight: _,
246            outbound_capacity_msat,
247            next_outbound_htlc_limit_msat,
248            next_outbound_htlc_minimum_msat: _,
249            inbound_capacity_msat,
250            confirmations_required: _,
251            confirmations: _,
252            force_close_spend_delay,
253            is_outbound,
254            is_channel_ready,
255            channel_shutdown_state: _,
256            is_usable,
257            is_announced,
258            inbound_htlc_minimum_msat,
259            inbound_htlc_maximum_msat,
260            config,
261            pending_inbound_htlcs: _,
262            pending_outbound_htlcs: _,
263        } = details;
264
265        let channel_id = LxChannelId::from(channel_id);
266        let user_channel_id = LxUserChannelId::from(user_channel_id);
267        let scid = short_channel_id.map(Scid);
268        let funding_txo = funding_txo.map(LxOutPoint::from);
269        let counterparty_node_id = NodePk(counterparty.node_id);
270        let channel_value = Amount::try_from_sats_u64(channel_value_satoshis)
271            .context("Channel value overflow")?;
272        let punishment_reserve = unspendable_punishment_reserve
273            .unwrap_or(0)
274            .apply(Amount::try_from_sats_u64)
275            .context("Punishment reserve overflow")?;
276        let is_ready = is_channel_ready;
277
278        let outbound_capacity = Amount::from_msat(outbound_capacity_msat);
279        let next_outbound_htlc_limit =
280            Amount::from_msat(next_outbound_htlc_limit_msat);
281
282        let their_balance = channel_value
283            .checked_sub(our_balance)
284            .context("Our balance was higher than the total channel value")?;
285        let inbound_capacity = Amount::from_msat(inbound_capacity_msat);
286
287        let config = config
288            // Only None prior to LDK 0.0.109
289            .context("Missing config")?;
290        let one_million = dec!(1_000_000);
291        let our_base_fee = config
292            .forwarding_fee_base_msat
293            .apply(u64::from)
294            .apply(Amount::from_msat);
295        let our_prop_fee =
296            Decimal::from(config.forwarding_fee_proportional_millionths)
297                / one_million;
298        let our_cltv_expiry_delta = config.cltv_expiry_delta;
299        let their_base_fee =
300            counterparty.forwarding_info.as_ref().map(|info| {
301                info.fee_base_msat.apply(u64::from).apply(Amount::from_msat)
302            });
303        let their_prop_fee =
304            counterparty.forwarding_info.as_ref().map(|info| {
305                Decimal::from(info.fee_proportional_millionths) / one_million
306            });
307        let their_cltv_expiry_delta = counterparty
308            .forwarding_info
309            .as_ref()
310            .map(|info| info.cltv_expiry_delta);
311
312        let inbound_htlc_minimum = inbound_htlc_minimum_msat
313            // Only None prior to LDK 0.0.107
314            .context("Missing inbound_htlc_minimum_msat")?
315            .apply(Amount::from_msat);
316        let inbound_htlc_maximum =
317            inbound_htlc_maximum_msat.map(Amount::from_msat);
318        let outbound_htlc_minimum = counterparty
319            .outbound_htlc_minimum_msat
320            .map(Amount::from_msat);
321        let outbound_htlc_maximum = counterparty
322            .outbound_htlc_maximum_msat
323            .map(Amount::from_msat);
324
325        let cpty_supports_basic_mpp =
326            counterparty.features.supports_basic_mpp();
327        let cpty_supports_onion_messages =
328            counterparty.features.supports_onion_messages();
329        let cpty_supports_wumbo = counterparty.features.supports_wumbo();
330        let cpty_supports_zero_conf =
331            counterparty.features.supports_zero_conf();
332
333        Ok(Self {
334            channel_id,
335            user_channel_id,
336            scid,
337            inbound_payment_scid,
338            outbound_payment_scid,
339            funding_txo,
340            counterparty_alias,
341            counterparty_node_id,
342            channel_value,
343            punishment_reserve,
344            force_close_spend_delay,
345            is_announced,
346            is_outbound,
347            is_ready,
348            is_usable,
349            our_balance,
350            outbound_capacity,
351            next_outbound_htlc_limit,
352
353            their_balance,
354            inbound_capacity,
355
356            our_base_fee,
357            our_prop_fee,
358            our_cltv_expiry_delta,
359            their_base_fee,
360            their_prop_fee,
361            their_cltv_expiry_delta,
362
363            inbound_htlc_minimum,
364            inbound_htlc_maximum,
365            outbound_htlc_minimum,
366            outbound_htlc_maximum,
367
368            cpty_supports_basic_mpp,
369            cpty_supports_onion_messages,
370            cpty_supports_wumbo,
371            cpty_supports_zero_conf,
372        })
373    }
374}
375
376/// A newtype for [`OutPoint`] that provides [`FromStr`] / [`Display`] impls.
377///
378/// Since the persister relies on the string representation to identify
379/// channels, having a newtype (instead of upstreaming these impls to LDK)
380/// ensures that the serialization scheme does not change from beneath us.
381#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
382#[derive(SerializeDisplay, DeserializeFromStr)]
383#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
384pub struct LxOutPoint {
385    pub txid: Txid,
386    pub index: u16,
387}
388
389impl From<OutPoint> for LxOutPoint {
390    fn from(op: OutPoint) -> Self {
391        Self {
392            txid: Txid(op.txid),
393            index: op.index,
394        }
395    }
396}
397
398impl From<LxOutPoint> for OutPoint {
399    fn from(op: LxOutPoint) -> Self {
400        Self {
401            txid: op.txid.0,
402            index: op.index,
403        }
404    }
405}
406
407impl From<LxOutPoint> for bitcoin::OutPoint {
408    fn from(op: LxOutPoint) -> Self {
409        bitcoin::OutPoint {
410            txid: op.txid.0,
411            vout: u32::from(op.index),
412        }
413    }
414}
415
416/// Deserializes from `<txid>_<index>`
417impl FromStr for LxOutPoint {
418    type Err = anyhow::Error;
419    fn from_str(outpoint_str: &str) -> anyhow::Result<Self> {
420        let mut txid_and_txindex = outpoint_str.split('_');
421        let txid_str = txid_and_txindex
422            .next()
423            .context("Missing <txid> in <txid>_<index>")?;
424        let index_str = txid_and_txindex
425            .next()
426            .context("Missing <index> in <txid>_<index>")?;
427
428        let txid = Txid::from_str(txid_str)
429            .context("Invalid txid returned from DB")?;
430        let index = u16::from_str(index_str)
431            .context("Could not parse index into u16")?;
432
433        Ok(Self { txid, index })
434    }
435}
436
437/// Serializes to `<txid>_<index>`
438impl Display for LxOutPoint {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        write!(f, "{}_{}", self.txid, self.index)
441    }
442}
443
444#[cfg(test)]
445mod test {
446    use super::*;
447    use crate::test_utils::roundtrip;
448    #[test]
449    fn outpoint_fromstr_display_roundtrip() {
450        roundtrip::fromstr_display_roundtrip_proptest::<LxOutPoint>();
451    }
452}