Skip to main content

lexe_common/ln/
channel.rs

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