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