Skip to main content

fedimint_ln_common/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::doc_markdown)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7
8//! # Lightning Module
9//!
10//! This module allows to atomically and trustlessly (in the federated trust
11//! model) interact with the Lightning network through a Lightning gateway.
12//!
13//! ## Attention: only one operation per contract and round
14//! If this module is active the consensus' conflict filter must ensure that at
15//! most one operation (spend, funding) happens per contract per round
16
17#[cfg(feature = "uniffi")]
18uniffi::setup_scaffolding!();
19
20pub mod client;
21pub mod config;
22pub mod contracts;
23pub mod federation_endpoint_constants;
24pub mod gateway_endpoint_constants;
25
26use std::collections::BTreeMap;
27use std::io::{Error, Read, Write};
28use std::time::{Duration, SystemTime};
29
30use anyhow::Context as AnyhowContext;
31use bitcoin::hashes::{Hash, sha256};
32use config::LightningClientConfig;
33use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
34use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
35use fedimint_core::module::registry::ModuleDecoderRegistry;
36use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
37use fedimint_core::secp256k1::Message;
38use fedimint_core::util::SafeUrl;
39use fedimint_core::{
40    Amount, PeerId, encode_bolt11_invoice_features_without_length,
41    extensible_associated_module_type, plugin_types_trait_impl_common, secp256k1,
42};
43use lightning_invoice::{Bolt11Invoice, RoutingFees};
44pub use reqwest::Method;
45use secp256k1::schnorr::Signature;
46use serde::{Deserialize, Serialize};
47use thiserror::Error;
48use threshold_crypto::PublicKey;
49pub use {bitcoin, lightning_invoice};
50
51use crate::contracts::incoming::OfferId;
52use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
53use crate::route_hints::RouteHint;
54
55pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
56pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 0);
57
58extensible_associated_module_type!(
59    LightningInput,
60    LightningInputV0,
61    UnknownLightningInputVariantError
62);
63
64impl LightningInput {
65    pub fn new_v0(
66        contract_id: ContractId,
67        amount: Amount,
68        witness: Option<Preimage>,
69    ) -> LightningInput {
70        LightningInput::V0(LightningInputV0 {
71            contract_id,
72            amount,
73            witness,
74        })
75    }
76}
77
78#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
79pub struct LightningInputV0 {
80    pub contract_id: contracts::ContractId,
81    /// While for now we only support spending the entire contract we need to
82    /// avoid
83    pub amount: Amount,
84    /// Of the three contract types only the outgoing one needs any other
85    /// witness data than a signature. The signature is aggregated on the
86    /// transaction level, so only the optional preimage remains.
87    pub witness: Option<Preimage>,
88}
89
90impl std::fmt::Display for LightningInputV0 {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        write!(
93            f,
94            "Lightning Contract {} with amount {}",
95            self.contract_id, self.amount
96        )
97    }
98}
99
100extensible_associated_module_type!(
101    LightningOutput,
102    LightningOutputV0,
103    UnknownLightningOutputVariantError
104);
105
106impl LightningOutput {
107    pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
108        LightningOutput::V0(LightningOutputV0::Contract(contract))
109    }
110
111    pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
112        LightningOutput::V0(LightningOutputV0::Offer(offer))
113    }
114
115    pub fn new_v0_cancel_outgoing(
116        contract: ContractId,
117        gateway_signature: secp256k1::schnorr::Signature,
118    ) -> LightningOutput {
119        LightningOutput::V0(LightningOutputV0::CancelOutgoing {
120            contract,
121            gateway_signature,
122        })
123    }
124}
125
126/// Represents an output of the Lightning module.
127///
128/// There are three sub-types:
129///   * Normal contracts users may lock funds in
130///   * Offers to buy preimages (see `contracts::incoming` docs)
131///   * Early cancellation of outgoing contracts before their timeout
132///
133/// The offer type exists to register `IncomingContractOffer`s. Instead of
134/// patching in a second way of letting clients submit consensus items outside
135/// of transactions we let offers be a 0-amount output. We need to take care to
136/// allow 0-input, 1-output transactions for that to allow users to receive
137/// their first notes via LN without already having notes.
138#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
139pub enum LightningOutputV0 {
140    /// Fund contract
141    Contract(ContractOutput),
142    /// Create incoming contract offer
143    Offer(contracts::incoming::IncomingContractOffer),
144    /// Allow early refund of outgoing contract
145    CancelOutgoing {
146        /// Contract to update
147        contract: ContractId,
148        /// Signature of gateway
149        gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
150    },
151}
152
153impl std::fmt::Display for LightningOutputV0 {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
157                Contract::Incoming(incoming) => {
158                    write!(
159                        f,
160                        "LN Incoming Contract for {} hash {}",
161                        amount, incoming.hash
162                    )
163                }
164                Contract::Outgoing(outgoing) => {
165                    write!(
166                        f,
167                        "LN Outgoing Contract for {} hash {}",
168                        amount, outgoing.hash
169                    )
170                }
171            },
172            LightningOutputV0::Offer(offer) => {
173                write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
174            }
175            LightningOutputV0::CancelOutgoing { contract, .. } => {
176                write!(f, "LN outgoing contract cancellation {contract}")
177            }
178        }
179    }
180}
181
182#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
183pub struct ContractOutput {
184    pub amount: fedimint_core::Amount,
185    pub contract: contracts::Contract,
186}
187
188#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
189pub struct ContractAccount {
190    pub amount: fedimint_core::Amount,
191    pub contract: contracts::FundedContract,
192}
193
194extensible_associated_module_type!(
195    LightningOutputOutcome,
196    LightningOutputOutcomeV0,
197    UnknownLightningOutputOutcomeVariantError
198);
199
200impl LightningOutputOutcome {
201    pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
202        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
203    }
204
205    pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
206        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
207    }
208
209    pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
210        LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
211    }
212}
213
214#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
215pub enum LightningOutputOutcomeV0 {
216    Contract {
217        id: ContractId,
218        outcome: ContractOutcome,
219    },
220    Offer {
221        id: OfferId,
222    },
223    CancelOutgoingContract {
224        id: ContractId,
225    },
226}
227
228impl LightningOutputOutcomeV0 {
229    pub fn is_permanent(&self) -> bool {
230        match self {
231            LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
232            LightningOutputOutcomeV0::Offer { .. }
233            | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
234        }
235    }
236}
237
238impl std::fmt::Display for LightningOutputOutcomeV0 {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        match self {
241            LightningOutputOutcomeV0::Contract { id, .. } => {
242                write!(f, "LN Contract {id}")
243            }
244            LightningOutputOutcomeV0::Offer { id } => {
245                write!(f, "LN Offer {id}")
246            }
247            LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
248                write!(f, "LN Outgoing Contract Cancellation {contract_id}")
249            }
250        }
251    }
252}
253
254/// Information about a gateway that is stored locally and expires based on
255/// local system time
256///
257/// Should only be serialized and deserialized in formats that can ignore
258/// additional fields as this struct may be extended in the future.
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
260pub struct LightningGatewayRegistration {
261    pub info: LightningGateway,
262    /// Indicates if this announcement has been vetted by the federation
263    pub vetted: bool,
264    /// Limits the validity of the announcement to allow updates, anchored to
265    /// local system time
266    pub valid_until: SystemTime,
267}
268
269impl Encodable for LightningGatewayRegistration {
270    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
271        let json_repr = serde_json::to_string(self).map_err(|e| {
272            Error::other(format!(
273                "Failed to serialize LightningGatewayRegistration: {e}"
274            ))
275        })?;
276
277        json_repr.consensus_encode(writer)
278    }
279}
280
281impl Decodable for LightningGatewayRegistration {
282    fn consensus_decode_partial<R: Read>(
283        r: &mut R,
284        modules: &ModuleDecoderRegistry,
285    ) -> Result<Self, DecodeError> {
286        let json_repr = String::consensus_decode_partial(r, modules)?;
287        serde_json::from_str(&json_repr).map_err(|e| {
288            DecodeError::new_custom(
289                anyhow::Error::new(e).context("Failed to deserialize LightningGatewayRegistration"),
290            )
291        })
292    }
293}
294
295impl LightningGatewayRegistration {
296    /// Create an announcement from this registration that is ttl-limited by
297    /// a floating duration. This is useful for sharing the announcement with
298    /// other nodes with unsynchronized clocks which can then anchor the
299    /// announcement to their local system time.
300    pub fn unanchor(self) -> LightningGatewayAnnouncement {
301        LightningGatewayAnnouncement {
302            info: self.info,
303            ttl: self
304                .valid_until
305                .duration_since(fedimint_core::time::now())
306                .unwrap_or_default(),
307            vetted: self.vetted,
308        }
309    }
310
311    pub fn is_expired(&self) -> bool {
312        self.valid_until < fedimint_core::time::now()
313    }
314}
315
316/// Information about a gateway that is shared with other federation members and
317/// expires based on a TTL to allow for sharing between nodes with
318/// unsynchronized clocks which can each anchor the announcement to their local
319/// system time.
320///
321/// Should only be serialized and deserialized in formats that can ignore
322/// additional fields as this struct may be extended in the future.
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
324#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
325pub struct LightningGatewayAnnouncement {
326    pub info: LightningGateway,
327    /// Indicates if this announcement has been vetted by the federation
328    pub vetted: bool,
329    /// Limits the validity of the announcement to allow updates, unanchored to
330    /// local system time to allow sharing between nodes with unsynchronized
331    /// clocks
332    pub ttl: Duration,
333}
334
335impl LightningGatewayAnnouncement {
336    /// Create a registration from this announcement that is anchored to the
337    /// local system time.
338    pub fn anchor(self) -> LightningGatewayRegistration {
339        LightningGatewayRegistration {
340            info: self.info,
341            vetted: self.vetted,
342            valid_until: fedimint_core::time::now() + self.ttl,
343        }
344    }
345}
346
347/// Information a gateway registers with a federation
348#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
349#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
350pub struct LightningGateway {
351    /// Unique per-federation identifier assigned by the gateway.
352    /// All clients in this federation should use this value as
353    /// `short_channel_id` when creating invoices to be settled by this
354    /// gateway.
355    #[serde(rename = "mint_channel_id")]
356    pub federation_index: u64,
357    /// Key used to pay the gateway
358    pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
359    pub node_pub_key: fedimint_core::secp256k1::PublicKey,
360    pub lightning_alias: String,
361    /// URL to the gateway's versioned public API
362    /// (e.g. <https://gateway.example.com/v1>)
363    pub api: SafeUrl,
364    /// Route hints to reach the LN node of the gateway.
365    ///
366    /// These will be appended with the route hint of the recipient's virtual
367    /// channel. To keeps invoices small these should be used sparingly.
368    pub route_hints: Vec<route_hints::RouteHint>,
369    /// Gateway configured routing fees
370    #[serde(with = "serde_routing_fees")]
371    pub fees: RoutingFees,
372    pub gateway_id: secp256k1::PublicKey,
373    /// Indicates if the gateway supports private payments
374    pub supports_private_payments: bool,
375}
376
377#[cfg(feature = "uniffi")]
378#[derive(uniffi::Record)]
379pub struct RoutingFeesFfi {
380    /// Flat routing fee in millisatoshis.
381    pub base_msat: u32,
382    /// Liquidity-based routing fee in millionths of a routed amount.
383    /// In other words, 10000 is 1%.
384    pub proportional_millionths: u32,
385}
386
387#[cfg(feature = "uniffi")]
388uniffi::custom_type!(RoutingFees, RoutingFeesFfi, {
389    remote,
390    lower: |fees| RoutingFeesFfi {
391        base_msat: fees.base_msat,
392        proportional_millionths: fees.proportional_millionths,
393    },
394    try_lift: |fees_ffi| Ok(RoutingFees {
395        base_msat: fees_ffi.base_msat,
396        proportional_millionths: fees_ffi.proportional_millionths,
397    }),
398});
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
401pub enum LightningConsensusItem {
402    DecryptPreimage(ContractId, PreimageDecryptionShare),
403    BlockCount(u64),
404    #[encodable_default]
405    Default {
406        variant: u64,
407        bytes: Vec<u8>,
408    },
409}
410
411impl std::fmt::Display for LightningConsensusItem {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        match self {
414            LightningConsensusItem::DecryptPreimage(contract_id, _) => {
415                write!(f, "LN Decryption Share - contract_id: {contract_id}")
416            }
417            LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
418            LightningConsensusItem::Default { variant, .. } => {
419                write!(f, "LN Unknown - variant={variant}")
420            }
421        }
422    }
423}
424
425#[derive(Debug)]
426pub struct LightningCommonInit;
427
428impl CommonModuleInit for LightningCommonInit {
429    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
430    const KIND: ModuleKind = KIND;
431
432    type ClientConfig = LightningClientConfig;
433
434    fn decoder() -> Decoder {
435        LightningModuleTypes::decoder()
436    }
437}
438
439pub struct LightningModuleTypes;
440
441plugin_types_trait_impl_common!(
442    KIND,
443    LightningModuleTypes,
444    LightningClientConfig,
445    LightningInput,
446    LightningOutput,
447    LightningOutputOutcome,
448    LightningConsensusItem,
449    LightningInputError,
450    LightningOutputError
451);
452
453// TODO: upstream serde support to LDK
454/// Hack to get a route hint that implements `serde` traits.
455pub mod route_hints {
456    #[cfg(feature = "uniffi")]
457    use std::str::FromStr;
458
459    use fedimint_core::encoding::{Decodable, Encodable};
460    use fedimint_core::secp256k1::PublicKey;
461    use lightning_invoice::RoutingFees;
462    use serde::{Deserialize, Serialize};
463
464    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
465    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
466    pub struct RouteHintHop {
467        /// The `node_id` of the non-target end of the route
468        pub src_node_id: PublicKey,
469        /// The `short_channel_id` of this channel
470        pub short_channel_id: u64,
471        /// Flat routing fee in millisatoshis
472        pub base_msat: u32,
473        /// Liquidity-based routing fee in millionths of a routed amount.
474        /// In other words, 10000 is 1%.
475        pub proportional_millionths: u32,
476        /// The difference in CLTV values between this node and the next node.
477        pub cltv_expiry_delta: u16,
478        /// The minimum value, in msat, which must be relayed to the next hop.
479        pub htlc_minimum_msat: Option<u64>,
480        /// The maximum value in msat available for routing with a single HTLC.
481        pub htlc_maximum_msat: Option<u64>,
482    }
483
484    #[cfg(feature = "uniffi")]
485    uniffi::custom_type!(PublicKey, String, {
486    remote,
487    lower: |pk| pk.to_string(),
488    try_lift: |s| PublicKey::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
489    });
490
491    /// A list of hops along a payment path terminating with a channel to the
492    /// recipient.
493    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
494    pub struct RouteHint(pub Vec<RouteHintHop>);
495
496    #[cfg(feature = "uniffi")]
497    uniffi::custom_newtype!(RouteHint, Vec<RouteHintHop>);
498
499    impl RouteHint {
500        pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
501            lightning_invoice::RouteHint(
502                self.0
503                    .iter()
504                    .map(|hop| lightning_invoice::RouteHintHop {
505                        src_node_id: hop.src_node_id,
506                        short_channel_id: hop.short_channel_id,
507                        fees: RoutingFees {
508                            base_msat: hop.base_msat,
509                            proportional_millionths: hop.proportional_millionths,
510                        },
511                        cltv_expiry_delta: hop.cltv_expiry_delta,
512                        htlc_minimum_msat: hop.htlc_minimum_msat,
513                        htlc_maximum_msat: hop.htlc_maximum_msat,
514                    })
515                    .collect(),
516            )
517        }
518    }
519
520    impl From<lightning_invoice::RouteHint> for RouteHint {
521        fn from(rh: lightning_invoice::RouteHint) -> Self {
522            RouteHint(rh.0.into_iter().map(Into::into).collect())
523        }
524    }
525
526    impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
527        fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
528            RouteHintHop {
529                src_node_id: rhh.src_node_id,
530                short_channel_id: rhh.short_channel_id,
531                base_msat: rhh.fees.base_msat,
532                proportional_millionths: rhh.fees.proportional_millionths,
533                cltv_expiry_delta: rhh.cltv_expiry_delta,
534                htlc_minimum_msat: rhh.htlc_minimum_msat,
535                htlc_maximum_msat: rhh.htlc_maximum_msat,
536            }
537        }
538    }
539}
540
541// TODO: Upstream serde serialization for
542// lightning_invoice::RoutingFees
543// See https://github.com/lightningdevkit/rust-lightning/blob/b8ed4d2608e32128dd5a1dee92911638a4301138/lightning/src/routing/gossip.rs#L1057-L1065
544pub mod serde_routing_fees {
545    use lightning_invoice::RoutingFees;
546    use serde::ser::SerializeStruct;
547    use serde::{Deserialize, Deserializer, Serializer};
548
549    #[allow(missing_docs)]
550    pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
551    where
552        S: Serializer,
553    {
554        let mut state = serializer.serialize_struct("RoutingFees", 2)?;
555        state.serialize_field("base_msat", &fees.base_msat)?;
556        state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
557        state.end()
558    }
559
560    #[allow(missing_docs)]
561    pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
562    where
563        D: Deserializer<'de>,
564    {
565        let fees = serde_json::Value::deserialize(deserializer)?;
566        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
567        let base_msat = fees["base_msat"]
568            .as_u64()
569            .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
570        let proportional_millionths = fees["proportional_millionths"]
571            .as_u64()
572            .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
573
574        Ok(RoutingFees {
575            base_msat: base_msat
576                .try_into()
577                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
578            proportional_millionths: proportional_millionths.try_into().map_err(|_| {
579                serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
580            })?,
581        })
582    }
583}
584
585pub mod serde_option_routing_fees {
586    use lightning_invoice::RoutingFees;
587    use serde::ser::SerializeStruct;
588    use serde::{Deserialize, Deserializer, Serializer};
589
590    #[allow(missing_docs)]
591    pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
592    where
593        S: Serializer,
594    {
595        if let Some(fees) = fees {
596            let mut state = serializer.serialize_struct("RoutingFees", 2)?;
597            state.serialize_field("base_msat", &fees.base_msat)?;
598            state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
599            state.end()
600        } else {
601            let state = serializer.serialize_struct("RoutingFees", 0)?;
602            state.end()
603        }
604    }
605
606    #[allow(missing_docs)]
607    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
608    where
609        D: Deserializer<'de>,
610    {
611        let fees = serde_json::Value::deserialize(deserializer)?;
612        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
613        let base_msat = fees["base_msat"].as_u64();
614
615        if let Some(base_msat) = base_msat
616            && let Some(proportional_millionths) = fees["proportional_millionths"].as_u64()
617        {
618            let base_msat: u32 = base_msat
619                .try_into()
620                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
621            let proportional_millionths: u32 =
622                proportional_millionths.try_into().map_err(|_| {
623                    serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
624                })?;
625            return Ok(Some(RoutingFees {
626                base_msat,
627                proportional_millionths,
628            }));
629        }
630
631        Ok(None)
632    }
633}
634
635#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
636pub enum LightningInputError {
637    #[error("The input contract {0} does not exist")]
638    UnknownContract(ContractId),
639    #[error("The input contract has too little funds, got {0}, input spends {1}")]
640    InsufficientFunds(Amount, Amount),
641    #[error("An outgoing LN contract spend did not provide a preimage")]
642    MissingPreimage,
643    #[error("An outgoing LN contract spend provided a wrong preimage")]
644    InvalidPreimage,
645    #[error("Incoming contract not ready to be spent yet, decryption in progress")]
646    ContractNotReady,
647    #[error("The lightning input version is not supported by this federation")]
648    UnknownInputVariant(#[from] UnknownLightningInputVariantError),
649}
650
651#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
652pub enum LightningOutputError {
653    #[error("The input contract {0} does not exist")]
654    UnknownContract(ContractId),
655    #[error("Output contract value may not be zero unless it's an offer output")]
656    ZeroOutput,
657    #[error("Offer contains invalid threshold-encrypted data")]
658    InvalidEncryptedPreimage,
659    #[error("Offer contains a ciphertext that has already been used")]
660    DuplicateEncryptedPreimage,
661    #[error("The incoming LN account requires more funding (need {0} got {1})")]
662    InsufficientIncomingFunding(Amount, Amount),
663    #[error("No offer found for payment hash {0}")]
664    NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
665    #[error("Only outgoing contracts support cancellation")]
666    NotOutgoingContract,
667    #[error("Cancellation request wasn't properly signed")]
668    InvalidCancellationSignature,
669    #[error("The lightning output version is not supported by this federation")]
670    UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
671}
672
673/// Data needed to pay an invoice
674///
675/// This is a subset of the data from a [`lightning_invoice::Bolt11Invoice`]
676/// that does not contain the description, which increases privacy for the user.
677#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
678pub struct PrunedInvoice {
679    pub amount: Amount,
680    pub destination: secp256k1::PublicKey,
681    /// Wire-format encoding of feature bit vector
682    #[serde(with = "fedimint_core::hex::serde", default)]
683    pub destination_features: Vec<u8>,
684    pub payment_hash: sha256::Hash,
685    pub payment_secret: [u8; 32],
686    pub route_hints: Vec<RouteHint>,
687    pub min_final_cltv_delta: u64,
688    /// Time at which the invoice expires in seconds since unix epoch
689    pub expiry_timestamp: u64,
690}
691
692impl PrunedInvoice {
693    pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
694        // We use expires_at since it doesn't rely on the std feature in
695        // lightning-invoice. See #3838.
696        let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
697
698        let destination_features = if let Some(features) = invoice.features() {
699            encode_bolt11_invoice_features_without_length(features)
700        } else {
701            vec![]
702        };
703
704        PrunedInvoice {
705            amount,
706            destination: invoice
707                .payee_pub_key()
708                .copied()
709                .unwrap_or_else(|| invoice.recover_payee_pub_key()),
710            destination_features,
711            payment_hash: *invoice.payment_hash(),
712            payment_secret: invoice.payment_secret().0,
713            route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
714            min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
715            expiry_timestamp,
716        }
717    }
718}
719
720impl TryFrom<Bolt11Invoice> for PrunedInvoice {
721    type Error = anyhow::Error;
722
723    fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
724        Ok(PrunedInvoice::new(
725            &invoice,
726            Amount::from_msats(
727                invoice
728                    .amount_milli_satoshis()
729                    .context("Invoice amount is missing")?,
730            ),
731        ))
732    }
733}
734
735/// Request sent to the federation that requests the removal of a gateway
736/// registration. Each peer is expected to check the `signatures` map for the
737/// signature that validates the gateway authorized the removal of this
738/// registration.
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct RemoveGatewayRequest {
741    pub gateway_id: secp256k1::PublicKey,
742    pub signatures: BTreeMap<PeerId, Signature>,
743}
744
745/// Creates a message to be signed by the Gateway's private key for the purpose
746/// of removing the gateway's registration record. Message is defined as:
747///
748/// msg = sha256(tag + federation_public_key + peer_id + challenge)
749///
750/// Tag is always `remove_gateway`. Challenge is unique for the registration
751/// record and acquired from each guardian prior to the removal request.
752pub fn create_gateway_remove_message(
753    federation_public_key: PublicKey,
754    peer_id: PeerId,
755    challenge: sha256::Hash,
756) -> Message {
757    let mut message_preimage = "remove-gateway".as_bytes().to_vec();
758    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
759    let guardian_id: u16 = peer_id.into();
760    message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
761    message_preimage.append(&mut challenge.consensus_encode_to_vec());
762    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
763}