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
26/// Exclusive remaining-CLTV safety margin for funding LNv1 incoming contracts.
27///
28/// Fresh intercepted HTLCs must have more remaining blocks than this margin.
29/// The value matches the route-hint delta advertised by pre-upgrade clients so
30/// that their invoices remain payable; once the deployed client fleet
31/// advertises [`LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA`], enforcement can
32/// be raised towards it.
33pub const LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN: u16 = 30;
34
35/// Route-hint CLTV delta advertised in newly created LNv1 invoices.
36///
37/// Deliberately larger than the enforced
38/// [`LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN`]: new invoices reserve enough
39/// time for federation funding, threshold decryption, Lightning settlement,
40/// and on-chain recovery, so a future release can raise enforcement to this
41/// value without breaking payments.
42pub const LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA: u16 = 144;
43
44use std::collections::BTreeMap;
45use std::io::{Error, Read, Write};
46use std::time::{Duration, SystemTime};
47
48use anyhow::Context as AnyhowContext;
49use bitcoin::hashes::{Hash, sha256};
50use config::LightningClientConfig;
51use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
52use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
53use fedimint_core::module::registry::ModuleDecoderRegistry;
54use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
55use fedimint_core::secp256k1::Message;
56use fedimint_core::util::SafeUrl;
57use fedimint_core::{
58    Amount, PeerId, encode_bolt11_invoice_features_without_length,
59    extensible_associated_module_type, plugin_types_trait_impl_common, secp256k1,
60};
61use lightning_invoice::{Bolt11Invoice, RoutingFees};
62pub use reqwest::Method;
63use secp256k1::schnorr::Signature;
64use serde::{Deserialize, Serialize};
65use thiserror::Error;
66use threshold_crypto::PublicKey;
67/// The federation's aggregate public key, as committed to by a gateway
68/// registration proof. Re-exported so callers can verify proofs without
69/// depending on `threshold_crypto` directly.
70pub use threshold_crypto::PublicKey as FederationPublicKey;
71pub use {bitcoin, lightning_invoice};
72
73use crate::contracts::incoming::OfferId;
74use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
75use crate::route_hints::RouteHint;
76
77pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
78pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 1);
79
80/// From this module consensus version on, a contract account is funded exactly
81/// once and no offer can be created for a payment hash whose incoming contract
82/// account already exists.
83///
84/// Contract ids do not commit to the full contract state — an incoming
85/// contract's id is its payment hash and an outgoing contract's id omits the
86/// amount — so before this version a second funding output for the same id
87/// topped up the existing account while keeping its state. For an incoming
88/// contract whose preimage decryption already reached a terminal state, that
89/// let the first contract's gateway or preimage holder sweep the new funds.
90pub const CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
91    ModuleConsensusVersion::new(2, 1);
92
93extensible_associated_module_type!(
94    LightningInput,
95    LightningInputV0,
96    UnknownLightningInputVariantError
97);
98
99impl LightningInput {
100    pub fn new_v0(
101        contract_id: ContractId,
102        amount: Amount,
103        witness: Option<Preimage>,
104    ) -> LightningInput {
105        LightningInput::V0(LightningInputV0 {
106            contract_id,
107            amount,
108            witness,
109        })
110    }
111}
112
113#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
114pub struct LightningInputV0 {
115    pub contract_id: contracts::ContractId,
116    /// While for now we only support spending the entire contract we need to
117    /// avoid
118    pub amount: Amount,
119    /// Of the three contract types only the outgoing one needs any other
120    /// witness data than a signature. The signature is aggregated on the
121    /// transaction level, so only the optional preimage remains.
122    pub witness: Option<Preimage>,
123}
124
125impl std::fmt::Display for LightningInputV0 {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(
128            f,
129            "Lightning Contract {} with amount {}",
130            self.contract_id, self.amount
131        )
132    }
133}
134
135extensible_associated_module_type!(
136    LightningOutput,
137    LightningOutputV0,
138    UnknownLightningOutputVariantError
139);
140
141impl LightningOutput {
142    pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
143        LightningOutput::V0(LightningOutputV0::Contract(contract))
144    }
145
146    pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
147        LightningOutput::V0(LightningOutputV0::Offer(offer))
148    }
149
150    pub fn new_v0_cancel_outgoing(
151        contract: ContractId,
152        gateway_signature: secp256k1::schnorr::Signature,
153    ) -> LightningOutput {
154        LightningOutput::V0(LightningOutputV0::CancelOutgoing {
155            contract,
156            gateway_signature,
157        })
158    }
159}
160
161/// Represents an output of the Lightning module.
162///
163/// There are three sub-types:
164///   * Normal contracts users may lock funds in
165///   * Offers to buy preimages (see `contracts::incoming` docs)
166///   * Early cancellation of outgoing contracts before their timeout
167///
168/// The offer type exists to register `IncomingContractOffer`s. Instead of
169/// patching in a second way of letting clients submit consensus items outside
170/// of transactions we let offers be a 0-amount output. We need to take care to
171/// allow 0-input, 1-output transactions for that to allow users to receive
172/// their first notes via LN without already having notes.
173#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
174pub enum LightningOutputV0 {
175    /// Fund contract
176    Contract(ContractOutput),
177    /// Create incoming contract offer
178    Offer(contracts::incoming::IncomingContractOffer),
179    /// Allow early refund of outgoing contract
180    CancelOutgoing {
181        /// Contract to update
182        contract: ContractId,
183        /// Signature of gateway
184        gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
185    },
186}
187
188impl std::fmt::Display for LightningOutputV0 {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
192                Contract::Incoming(incoming) => {
193                    write!(
194                        f,
195                        "LN Incoming Contract for {} hash {}",
196                        amount, incoming.hash
197                    )
198                }
199                Contract::Outgoing(outgoing) => {
200                    write!(
201                        f,
202                        "LN Outgoing Contract for {} hash {}",
203                        amount, outgoing.hash
204                    )
205                }
206            },
207            LightningOutputV0::Offer(offer) => {
208                write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
209            }
210            LightningOutputV0::CancelOutgoing { contract, .. } => {
211                write!(f, "LN outgoing contract cancellation {contract}")
212            }
213        }
214    }
215}
216
217#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
218pub struct ContractOutput {
219    pub amount: fedimint_core::Amount,
220    pub contract: contracts::Contract,
221}
222
223#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
224pub struct ContractAccount {
225    pub amount: fedimint_core::Amount,
226    pub contract: contracts::FundedContract,
227}
228
229extensible_associated_module_type!(
230    LightningOutputOutcome,
231    LightningOutputOutcomeV0,
232    UnknownLightningOutputOutcomeVariantError
233);
234
235impl LightningOutputOutcome {
236    pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
237        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
238    }
239
240    pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
241        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
242    }
243
244    pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
245        LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
246    }
247}
248
249#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
250pub enum LightningOutputOutcomeV0 {
251    Contract {
252        id: ContractId,
253        outcome: ContractOutcome,
254    },
255    Offer {
256        id: OfferId,
257    },
258    CancelOutgoingContract {
259        id: ContractId,
260    },
261}
262
263impl LightningOutputOutcomeV0 {
264    pub fn is_permanent(&self) -> bool {
265        match self {
266            LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
267            LightningOutputOutcomeV0::Offer { .. }
268            | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
269        }
270    }
271}
272
273impl std::fmt::Display for LightningOutputOutcomeV0 {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        match self {
276            LightningOutputOutcomeV0::Contract { id, .. } => {
277                write!(f, "LN Contract {id}")
278            }
279            LightningOutputOutcomeV0::Offer { id } => {
280                write!(f, "LN Offer {id}")
281            }
282            LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
283                write!(f, "LN Outgoing Contract Cancellation {contract_id}")
284            }
285        }
286    }
287}
288
289/// Proof that a gateway registration was authorized by the holder of the
290/// secret key behind [`LightningGateway::gateway_id`].
291///
292/// Optional for backwards compatibility: gateways predating this field register
293/// unsigned, and guardians keep accepting them. A registration carrying a valid
294/// proof cannot be overwritten by an unsigned one, so a gateway becomes immune
295/// to identity hijacking as soon as it upgrades, without needing any other
296/// gateway, guardian or client to upgrade with it.
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
298#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
299pub struct GatewayRegistrationAuth {
300    /// Strictly increasing per gateway, so a captured signature cannot be
301    /// replayed to roll a registration back to stale settings. Guardians only
302    /// ever compare this against the value they already stored, never against
303    /// their own clock, so gateway and guardian clocks need not agree.
304    pub nonce: u64,
305    /// Schnorr signature over [`create_gateway_registration_message`].
306    pub signature: Signature,
307}
308
309/// Information about a gateway that is stored locally and expires based on
310/// local system time
311///
312/// Should only be serialized and deserialized in formats that can ignore
313/// additional fields as this struct may be extended in the future.
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
315pub struct LightningGatewayRegistration {
316    pub info: LightningGateway,
317    /// Indicates if this announcement has been vetted by the federation
318    pub vetted: bool,
319    /// Limits the validity of the announcement to allow updates, anchored to
320    /// local system time
321    pub valid_until: SystemTime,
322    /// Proof of possession of `info.gateway_id`, absent for registrations made
323    /// by gateways that predate it.
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub auth: Option<GatewayRegistrationAuth>,
326}
327
328impl Encodable for LightningGatewayRegistration {
329    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
330        let json_repr = serde_json::to_string(self).map_err(|e| {
331            Error::other(format!(
332                "Failed to serialize LightningGatewayRegistration: {e}"
333            ))
334        })?;
335
336        json_repr.consensus_encode(writer)
337    }
338}
339
340impl Decodable for LightningGatewayRegistration {
341    fn consensus_decode_partial<R: Read>(
342        r: &mut R,
343        modules: &ModuleDecoderRegistry,
344    ) -> Result<Self, DecodeError> {
345        let json_repr = String::consensus_decode_partial(r, modules)?;
346        serde_json::from_str(&json_repr).map_err(|e| {
347            DecodeError::new_custom(
348                anyhow::Error::new(e).context("Failed to deserialize LightningGatewayRegistration"),
349            )
350        })
351    }
352}
353
354impl LightningGatewayRegistration {
355    /// Create an announcement from this registration that is ttl-limited by
356    /// a floating duration. This is useful for sharing the announcement with
357    /// other nodes with unsynchronized clocks which can then anchor the
358    /// announcement to their local system time.
359    pub fn unanchor(self) -> LightningGatewayAnnouncement {
360        LightningGatewayAnnouncement {
361            info: self.info,
362            ttl: self
363                .valid_until
364                .duration_since(fedimint_core::time::now())
365                .unwrap_or_default(),
366            vetted: self.vetted,
367            auth: self.auth,
368        }
369    }
370
371    pub fn is_expired(&self) -> bool {
372        self.valid_until < fedimint_core::time::now()
373    }
374}
375
376/// Information about a gateway that is shared with other federation members and
377/// expires based on a TTL to allow for sharing between nodes with
378/// unsynchronized clocks which can each anchor the announcement to their local
379/// system time.
380///
381/// Should only be serialized and deserialized in formats that can ignore
382/// additional fields as this struct may be extended in the future.
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
384#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
385pub struct LightningGatewayAnnouncement {
386    pub info: LightningGateway,
387    /// Indicates if this announcement has been vetted by the federation
388    pub vetted: bool,
389    /// Limits the validity of the announcement to allow updates, unanchored to
390    /// local system time to allow sharing between nodes with unsynchronized
391    /// clocks
392    pub ttl: Duration,
393    /// Proof of possession of `info.gateway_id`, absent for gateways that
394    /// predate it.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub auth: Option<GatewayRegistrationAuth>,
397}
398
399#[cfg(feature = "uniffi")]
400use std::str::FromStr;
401
402#[cfg(feature = "uniffi")]
403uniffi::custom_type!(Signature, String, {
404    remote,
405    lower: |sig| sig.to_string(),
406    try_lift: |s| Signature::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
407});
408
409/// Upper bound guardians place on a registration's lifetime. Gateways announce
410/// a TTL two orders of magnitude below this, so it only ever binds on
411/// announcements that are trying to squat a `gateway_id` indefinitely.
412pub const MAX_GATEWAY_REGISTRATION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
413
414impl LightningGatewayAnnouncement {
415    /// Whether this announcement's proof of possession holds, treating an
416    /// announcement without one as valid — unsigned registrations are still
417    /// accepted, they simply earn no preference.
418    ///
419    /// Callers must apply this *before* preferring signed announcements over
420    /// unsigned ones. A proof that is merely present is worthless: anyone can
421    /// attach a garbage signature to someone else's `gateway_id`, so preferring
422    /// on presence alone lets a single peer evict every honest unsigned
423    /// announcement for a gateway.
424    pub fn registration_proof_is_valid(&self, federation_public_key: PublicKey) -> bool {
425        let Some(auth) = &self.auth else {
426            return true;
427        };
428
429        let msg =
430            create_gateway_registration_message(federation_public_key, auth.nonce, &self.info);
431
432        auth.signature
433            .verify(&msg, &self.info.gateway_id.x_only_public_key().0)
434            .is_ok()
435    }
436
437    /// Create a registration from this announcement that is anchored to the
438    /// local system time.
439    ///
440    /// The TTL is clamped to [`MAX_GATEWAY_REGISTRATION_TTL`], which also keeps
441    /// the addition below from having to handle an attacker-supplied
442    /// [`Duration`] large enough to overflow [`SystemTime`].
443    pub fn anchor(self) -> LightningGatewayRegistration {
444        let ttl = self.ttl.min(MAX_GATEWAY_REGISTRATION_TTL);
445
446        LightningGatewayRegistration {
447            info: self.info,
448            vetted: self.vetted,
449            valid_until: fedimint_core::time::now() + ttl,
450            auth: self.auth,
451        }
452    }
453}
454
455/// Information a gateway registers with a federation
456#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
457#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
458pub struct LightningGateway {
459    /// Unique per-federation identifier assigned by the gateway.
460    /// All clients in this federation should use this value as
461    /// `short_channel_id` when creating invoices to be settled by this
462    /// gateway.
463    #[serde(rename = "mint_channel_id")]
464    pub federation_index: u64,
465    /// Key used to pay the gateway
466    pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
467    pub node_pub_key: fedimint_core::secp256k1::PublicKey,
468    pub lightning_alias: String,
469    /// URL to the gateway's versioned public API
470    /// (e.g. <https://gateway.example.com/v1>)
471    pub api: SafeUrl,
472    /// Route hints to reach the LN node of the gateway.
473    ///
474    /// These will be appended with the route hint of the recipient's virtual
475    /// channel. To keeps invoices small these should be used sparingly.
476    pub route_hints: Vec<route_hints::RouteHint>,
477    /// Gateway configured routing fees
478    #[serde(with = "serde_routing_fees")]
479    pub fees: RoutingFees,
480    pub gateway_id: secp256k1::PublicKey,
481    /// Indicates if the gateway supports private payments
482    pub supports_private_payments: bool,
483}
484
485#[cfg(feature = "uniffi")]
486#[derive(uniffi::Record)]
487pub struct RoutingFeesFfi {
488    /// Flat routing fee in millisatoshis.
489    pub base_msat: u32,
490    /// Liquidity-based routing fee in millionths of a routed amount.
491    /// In other words, 10000 is 1%.
492    pub proportional_millionths: u32,
493}
494
495#[cfg(feature = "uniffi")]
496uniffi::custom_type!(RoutingFees, RoutingFeesFfi, {
497    remote,
498    lower: |fees| RoutingFeesFfi {
499        base_msat: fees.base_msat,
500        proportional_millionths: fees.proportional_millionths,
501    },
502    try_lift: |fees_ffi| Ok(RoutingFees {
503        base_msat: fees_ffi.base_msat,
504        proportional_millionths: fees_ffi.proportional_millionths,
505    }),
506});
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
509pub enum LightningConsensusItem {
510    DecryptPreimage(ContractId, PreimageDecryptionShare),
511    BlockCount(u64),
512    ModuleConsensusVersion(ModuleConsensusVersion),
513    #[encodable_default]
514    Default {
515        variant: u64,
516        bytes: Vec<u8>,
517    },
518}
519
520impl std::fmt::Display for LightningConsensusItem {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        match self {
523            LightningConsensusItem::DecryptPreimage(contract_id, _) => {
524                write!(f, "LN Decryption Share - contract_id: {contract_id}")
525            }
526            LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
527            LightningConsensusItem::ModuleConsensusVersion(version) => {
528                write!(
529                    f,
530                    "LN Consensus Version {}.{}",
531                    version.major, version.minor
532                )
533            }
534            LightningConsensusItem::Default { variant, .. } => {
535                write!(f, "LN Unknown - variant={variant}")
536            }
537        }
538    }
539}
540
541#[derive(Debug)]
542pub struct LightningCommonInit;
543
544impl CommonModuleInit for LightningCommonInit {
545    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
546    const KIND: ModuleKind = KIND;
547
548    type ClientConfig = LightningClientConfig;
549
550    fn decoder() -> Decoder {
551        LightningModuleTypes::decoder()
552    }
553}
554
555pub struct LightningModuleTypes;
556
557plugin_types_trait_impl_common!(
558    KIND,
559    LightningModuleTypes,
560    LightningClientConfig,
561    LightningInput,
562    LightningOutput,
563    LightningOutputOutcome,
564    LightningConsensusItem,
565    LightningInputError,
566    LightningOutputError
567);
568
569// TODO: upstream serde support to LDK
570/// Hack to get a route hint that implements `serde` traits.
571pub mod route_hints {
572    #[cfg(feature = "uniffi")]
573    use std::str::FromStr;
574
575    use fedimint_core::encoding::{Decodable, Encodable};
576    use fedimint_core::secp256k1::PublicKey;
577    use lightning_invoice::RoutingFees;
578    use serde::{Deserialize, Serialize};
579
580    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
581    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
582    pub struct RouteHintHop {
583        /// The `node_id` of the non-target end of the route
584        pub src_node_id: PublicKey,
585        /// The `short_channel_id` of this channel
586        pub short_channel_id: u64,
587        /// Flat routing fee in millisatoshis
588        pub base_msat: u32,
589        /// Liquidity-based routing fee in millionths of a routed amount.
590        /// In other words, 10000 is 1%.
591        pub proportional_millionths: u32,
592        /// The difference in CLTV values between this node and the next node.
593        pub cltv_expiry_delta: u16,
594        /// The minimum value, in msat, which must be relayed to the next hop.
595        pub htlc_minimum_msat: Option<u64>,
596        /// The maximum value in msat available for routing with a single HTLC.
597        pub htlc_maximum_msat: Option<u64>,
598    }
599
600    #[cfg(feature = "uniffi")]
601    uniffi::custom_type!(PublicKey, String, {
602    remote,
603    lower: |pk| pk.to_string(),
604    try_lift: |s| PublicKey::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
605    });
606
607    /// A list of hops along a payment path terminating with a channel to the
608    /// recipient.
609    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
610    pub struct RouteHint(pub Vec<RouteHintHop>);
611
612    #[cfg(feature = "uniffi")]
613    uniffi::custom_newtype!(RouteHint, Vec<RouteHintHop>);
614
615    impl RouteHint {
616        pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
617            lightning_invoice::RouteHint(
618                self.0
619                    .iter()
620                    .map(|hop| lightning_invoice::RouteHintHop {
621                        src_node_id: hop.src_node_id,
622                        short_channel_id: hop.short_channel_id,
623                        fees: RoutingFees {
624                            base_msat: hop.base_msat,
625                            proportional_millionths: hop.proportional_millionths,
626                        },
627                        cltv_expiry_delta: hop.cltv_expiry_delta,
628                        htlc_minimum_msat: hop.htlc_minimum_msat,
629                        htlc_maximum_msat: hop.htlc_maximum_msat,
630                    })
631                    .collect(),
632            )
633        }
634    }
635
636    impl From<lightning_invoice::RouteHint> for RouteHint {
637        fn from(rh: lightning_invoice::RouteHint) -> Self {
638            RouteHint(rh.0.into_iter().map(Into::into).collect())
639        }
640    }
641
642    impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
643        fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
644            RouteHintHop {
645                src_node_id: rhh.src_node_id,
646                short_channel_id: rhh.short_channel_id,
647                base_msat: rhh.fees.base_msat,
648                proportional_millionths: rhh.fees.proportional_millionths,
649                cltv_expiry_delta: rhh.cltv_expiry_delta,
650                htlc_minimum_msat: rhh.htlc_minimum_msat,
651                htlc_maximum_msat: rhh.htlc_maximum_msat,
652            }
653        }
654    }
655}
656
657// TODO: Upstream serde serialization for
658// lightning_invoice::RoutingFees
659// See https://github.com/lightningdevkit/rust-lightning/blob/b8ed4d2608e32128dd5a1dee92911638a4301138/lightning/src/routing/gossip.rs#L1057-L1065
660pub mod serde_routing_fees {
661    use lightning_invoice::RoutingFees;
662    use serde::ser::SerializeStruct;
663    use serde::{Deserialize, Deserializer, Serializer};
664
665    #[allow(missing_docs)]
666    pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
667    where
668        S: Serializer,
669    {
670        let mut state = serializer.serialize_struct("RoutingFees", 2)?;
671        state.serialize_field("base_msat", &fees.base_msat)?;
672        state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
673        state.end()
674    }
675
676    #[allow(missing_docs)]
677    pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
678    where
679        D: Deserializer<'de>,
680    {
681        let fees = serde_json::Value::deserialize(deserializer)?;
682        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
683        let base_msat = fees["base_msat"]
684            .as_u64()
685            .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
686        let proportional_millionths = fees["proportional_millionths"]
687            .as_u64()
688            .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
689
690        Ok(RoutingFees {
691            base_msat: base_msat
692                .try_into()
693                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
694            proportional_millionths: proportional_millionths.try_into().map_err(|_| {
695                serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
696            })?,
697        })
698    }
699}
700
701pub mod serde_option_routing_fees {
702    use lightning_invoice::RoutingFees;
703    use serde::ser::SerializeStruct;
704    use serde::{Deserialize, Deserializer, Serializer};
705
706    #[allow(missing_docs)]
707    pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
708    where
709        S: Serializer,
710    {
711        if let Some(fees) = fees {
712            let mut state = serializer.serialize_struct("RoutingFees", 2)?;
713            state.serialize_field("base_msat", &fees.base_msat)?;
714            state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
715            state.end()
716        } else {
717            let state = serializer.serialize_struct("RoutingFees", 0)?;
718            state.end()
719        }
720    }
721
722    #[allow(missing_docs)]
723    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
724    where
725        D: Deserializer<'de>,
726    {
727        let fees = serde_json::Value::deserialize(deserializer)?;
728        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
729        let base_msat = fees["base_msat"].as_u64();
730
731        if let Some(base_msat) = base_msat
732            && let Some(proportional_millionths) = fees["proportional_millionths"].as_u64()
733        {
734            let base_msat: u32 = base_msat
735                .try_into()
736                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
737            let proportional_millionths: u32 =
738                proportional_millionths.try_into().map_err(|_| {
739                    serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
740                })?;
741            return Ok(Some(RoutingFees {
742                base_msat,
743                proportional_millionths,
744            }));
745        }
746
747        Ok(None)
748    }
749}
750
751#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
752pub enum LightningInputError {
753    #[error("The input contract {0} does not exist")]
754    UnknownContract(ContractId),
755    #[error("The input contract has too little funds, got {0}, input spends {1}")]
756    InsufficientFunds(Amount, Amount),
757    #[error("An outgoing LN contract spend did not provide a preimage")]
758    MissingPreimage,
759    #[error("An outgoing LN contract spend provided a wrong preimage")]
760    InvalidPreimage,
761    #[error("Incoming contract not ready to be spent yet, decryption in progress")]
762    ContractNotReady,
763    #[error("The lightning input version is not supported by this federation")]
764    UnknownInputVariant(#[from] UnknownLightningInputVariantError),
765}
766
767#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
768pub enum LightningOutputError {
769    #[error("The input contract {0} does not exist")]
770    UnknownContract(ContractId),
771    #[error("Output contract value may not be zero unless it's an offer output")]
772    ZeroOutput,
773    #[error("Offer contains invalid threshold-encrypted data")]
774    InvalidEncryptedPreimage,
775    #[error("Offer contains a ciphertext that has already been used")]
776    DuplicateEncryptedPreimage,
777    #[error("The incoming LN account requires more funding (need {0} got {1})")]
778    InsufficientIncomingFunding(Amount, Amount),
779    #[error("No offer found for payment hash {0}")]
780    NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
781    #[error("Only outgoing contracts support cancellation")]
782    NotOutgoingContract,
783    #[error("Cancellation request wasn't properly signed")]
784    InvalidCancellationSignature,
785    #[error("The lightning output version is not supported by this federation")]
786    UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
787    // New variants have to be appended, the `Encodable` derive assigns indices by
788    // declaration order and clients decode this type from the API response.
789    #[error("The incoming contract {0} has already been funded")]
790    ContractAlreadyFunded(ContractId),
791    #[error("An incoming contract account for the offer's payment hash already exists: {0}")]
792    OfferForFundedContract(ContractId),
793    #[error("The contract's encrypted preimage does not match the offer's")]
794    EncryptedPreimageMismatch,
795    #[error("An incoming contract must be funded with a pending decryption")]
796    PreDecryptedIncomingContract,
797}
798
799/// Data needed to pay an invoice
800///
801/// This is a subset of the data from a [`lightning_invoice::Bolt11Invoice`]
802/// that does not contain the description, which increases privacy for the user.
803#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
804pub struct PrunedInvoice {
805    pub amount: Amount,
806    pub destination: secp256k1::PublicKey,
807    /// Wire-format encoding of feature bit vector
808    #[serde(with = "fedimint_core::hex::serde", default)]
809    pub destination_features: Vec<u8>,
810    pub payment_hash: sha256::Hash,
811    pub payment_secret: [u8; 32],
812    pub route_hints: Vec<RouteHint>,
813    pub min_final_cltv_delta: u64,
814    /// Time at which the invoice expires in seconds since unix epoch
815    pub expiry_timestamp: u64,
816}
817
818impl PrunedInvoice {
819    pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
820        // We use expires_at since it doesn't rely on the std feature in
821        // lightning-invoice. See #3838.
822        let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
823
824        let destination_features = if let Some(features) = invoice.features() {
825            encode_bolt11_invoice_features_without_length(features)
826        } else {
827            vec![]
828        };
829
830        PrunedInvoice {
831            amount,
832            destination: invoice
833                .payee_pub_key()
834                .copied()
835                .unwrap_or_else(|| invoice.recover_payee_pub_key()),
836            destination_features,
837            payment_hash: *invoice.payment_hash(),
838            payment_secret: invoice.payment_secret().0,
839            route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
840            min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
841            expiry_timestamp,
842        }
843    }
844}
845
846impl TryFrom<Bolt11Invoice> for PrunedInvoice {
847    type Error = anyhow::Error;
848
849    fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
850        Ok(PrunedInvoice::new(
851            &invoice,
852            Amount::from_msats(
853                invoice
854                    .amount_milli_satoshis()
855                    .context("Invoice amount is missing")?,
856            ),
857        ))
858    }
859}
860
861/// Request sent to the federation that requests the removal of a gateway
862/// registration. Each peer is expected to check the `signatures` map for the
863/// signature that validates the gateway authorized the removal of this
864/// registration.
865#[derive(Debug, Clone, Serialize, Deserialize)]
866pub struct RemoveGatewayRequest {
867    pub gateway_id: secp256k1::PublicKey,
868    pub signatures: BTreeMap<PeerId, Signature>,
869}
870
871/// Creates a message to be signed by the gateway's private key to prove that a
872/// registration was authorized by the holder of `info.gateway_id`. Message is
873/// defined as:
874///
875/// msg = sha256(tag + federation_public_key + nonce + info)
876///
877/// Tag is always `register-gateway`. Binding the federation public key stops a
878/// registration being replayed at a different federation, and the nonce stops
879/// an old registration being replayed at the same one.
880///
881/// Note this deliberately covers `info` but *not* the announcement's `ttl` or
882/// `vetted` flag. `ttl` is recomputed from `valid_until` every time a
883/// registration is read back ([`LightningGatewayRegistration::unanchor`]), so a
884/// signature over it would not survive being served to clients; it is bounded
885/// by [`MAX_GATEWAY_REGISTRATION_TTL`] instead. `vetted` is not the gateway's
886/// to assert, and guardians clear it on registration.
887pub fn create_gateway_registration_message(
888    federation_public_key: PublicKey,
889    nonce: u64,
890    info: &LightningGateway,
891) -> Message {
892    let mut message_preimage = "register-gateway".as_bytes().to_vec();
893    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
894    message_preimage.append(&mut nonce.consensus_encode_to_vec());
895    message_preimage.append(&mut info.consensus_encode_to_vec());
896    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
897}
898
899/// Creates a message to be signed by the Gateway's private key for the purpose
900/// of removing the gateway's registration record. Message is defined as:
901///
902/// msg = sha256(tag + federation_public_key + peer_id + challenge)
903///
904/// Tag is always `remove_gateway`. Challenge is unique for the registration
905/// record and acquired from each guardian prior to the removal request.
906pub fn create_gateway_remove_message(
907    federation_public_key: PublicKey,
908    peer_id: PeerId,
909    challenge: sha256::Hash,
910) -> Message {
911    let mut message_preimage = "remove-gateway".as_bytes().to_vec();
912    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
913    let guardian_id: u16 = peer_id.into();
914    message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
915    message_preimage.append(&mut challenge.consensus_encode_to_vec());
916    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
917}