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, DecodeContext, 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        DecodeContext::context(
347            serde_json::from_str(&json_repr).map_err(DecodeError::from_err),
348            "Failed to deserialize LightningGatewayRegistration",
349        )
350    }
351}
352
353impl LightningGatewayRegistration {
354    /// Create an announcement from this registration that is ttl-limited by
355    /// a floating duration. This is useful for sharing the announcement with
356    /// other nodes with unsynchronized clocks which can then anchor the
357    /// announcement to their local system time.
358    pub fn unanchor(self) -> LightningGatewayAnnouncement {
359        LightningGatewayAnnouncement {
360            info: self.info,
361            ttl: self
362                .valid_until
363                .duration_since(fedimint_core::time::now())
364                .unwrap_or_default(),
365            vetted: self.vetted,
366            auth: self.auth,
367        }
368    }
369
370    pub fn is_expired(&self) -> bool {
371        self.valid_until < fedimint_core::time::now()
372    }
373}
374
375/// Information about a gateway that is shared with other federation members and
376/// expires based on a TTL to allow for sharing between nodes with
377/// unsynchronized clocks which can each anchor the announcement to their local
378/// system time.
379///
380/// Should only be serialized and deserialized in formats that can ignore
381/// additional fields as this struct may be extended in the future.
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
383#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
384pub struct LightningGatewayAnnouncement {
385    pub info: LightningGateway,
386    /// Indicates if this announcement has been vetted by the federation
387    pub vetted: bool,
388    /// Limits the validity of the announcement to allow updates, unanchored to
389    /// local system time to allow sharing between nodes with unsynchronized
390    /// clocks
391    pub ttl: Duration,
392    /// Proof of possession of `info.gateway_id`, absent for gateways that
393    /// predate it.
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub auth: Option<GatewayRegistrationAuth>,
396}
397
398#[cfg(feature = "uniffi")]
399use std::str::FromStr;
400
401#[cfg(feature = "uniffi")]
402uniffi::custom_type!(Signature, String, {
403    remote,
404    lower: |sig| sig.to_string(),
405    try_lift: |s| Signature::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
406});
407
408/// Upper bound guardians place on a registration's lifetime. Gateways announce
409/// a TTL two orders of magnitude below this, so it only ever binds on
410/// announcements that are trying to squat a `gateway_id` indefinitely.
411pub const MAX_GATEWAY_REGISTRATION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
412
413impl LightningGatewayAnnouncement {
414    /// Whether this announcement's proof of possession holds, treating an
415    /// announcement without one as valid — unsigned registrations are still
416    /// accepted, they simply earn no preference.
417    ///
418    /// Callers must apply this *before* preferring signed announcements over
419    /// unsigned ones. A proof that is merely present is worthless: anyone can
420    /// attach a garbage signature to someone else's `gateway_id`, so preferring
421    /// on presence alone lets a single peer evict every honest unsigned
422    /// announcement for a gateway.
423    pub fn registration_proof_is_valid(&self, federation_public_key: PublicKey) -> bool {
424        let Some(auth) = &self.auth else {
425            return true;
426        };
427
428        let msg =
429            create_gateway_registration_message(federation_public_key, auth.nonce, &self.info);
430
431        auth.signature
432            .verify(&msg, &self.info.gateway_id.x_only_public_key().0)
433            .is_ok()
434    }
435
436    /// Create a registration from this announcement that is anchored to the
437    /// local system time.
438    ///
439    /// The TTL is clamped to [`MAX_GATEWAY_REGISTRATION_TTL`], which also keeps
440    /// the addition below from having to handle an attacker-supplied
441    /// [`Duration`] large enough to overflow [`SystemTime`].
442    pub fn anchor(self) -> LightningGatewayRegistration {
443        let ttl = self.ttl.min(MAX_GATEWAY_REGISTRATION_TTL);
444
445        LightningGatewayRegistration {
446            info: self.info,
447            vetted: self.vetted,
448            valid_until: fedimint_core::time::now() + ttl,
449            auth: self.auth,
450        }
451    }
452}
453
454/// Information a gateway registers with a federation
455#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
456#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
457pub struct LightningGateway {
458    /// Unique per-federation identifier assigned by the gateway.
459    /// All clients in this federation should use this value as
460    /// `short_channel_id` when creating invoices to be settled by this
461    /// gateway.
462    #[serde(rename = "mint_channel_id")]
463    pub federation_index: u64,
464    /// Key used to pay the gateway
465    pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
466    pub node_pub_key: fedimint_core::secp256k1::PublicKey,
467    pub lightning_alias: String,
468    /// URL to the gateway's versioned public API
469    /// (e.g. <https://gateway.example.com/v1>)
470    pub api: SafeUrl,
471    /// Route hints to reach the LN node of the gateway.
472    ///
473    /// These will be appended with the route hint of the recipient's virtual
474    /// channel. To keeps invoices small these should be used sparingly.
475    pub route_hints: Vec<route_hints::RouteHint>,
476    /// Gateway configured routing fees
477    #[serde(with = "serde_routing_fees")]
478    pub fees: RoutingFees,
479    pub gateway_id: secp256k1::PublicKey,
480    /// Indicates if the gateway supports private payments
481    pub supports_private_payments: bool,
482}
483
484#[cfg(feature = "uniffi")]
485#[derive(uniffi::Record)]
486pub struct RoutingFeesFfi {
487    /// Flat routing fee in millisatoshis.
488    pub base_msat: u32,
489    /// Liquidity-based routing fee in millionths of a routed amount.
490    /// In other words, 10000 is 1%.
491    pub proportional_millionths: u32,
492}
493
494#[cfg(feature = "uniffi")]
495uniffi::custom_type!(RoutingFees, RoutingFeesFfi, {
496    remote,
497    lower: |fees| RoutingFeesFfi {
498        base_msat: fees.base_msat,
499        proportional_millionths: fees.proportional_millionths,
500    },
501    try_lift: |fees_ffi| Ok(RoutingFees {
502        base_msat: fees_ffi.base_msat,
503        proportional_millionths: fees_ffi.proportional_millionths,
504    }),
505});
506
507#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
508pub enum LightningConsensusItem {
509    DecryptPreimage(ContractId, PreimageDecryptionShare),
510    BlockCount(u64),
511    ModuleConsensusVersion(ModuleConsensusVersion),
512    #[encodable_default]
513    Default {
514        variant: u64,
515        bytes: Vec<u8>,
516    },
517}
518
519impl std::fmt::Display for LightningConsensusItem {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        match self {
522            LightningConsensusItem::DecryptPreimage(contract_id, _) => {
523                write!(f, "LN Decryption Share - contract_id: {contract_id}")
524            }
525            LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
526            LightningConsensusItem::ModuleConsensusVersion(version) => {
527                write!(
528                    f,
529                    "LN Consensus Version {}.{}",
530                    version.major, version.minor
531                )
532            }
533            LightningConsensusItem::Default { variant, .. } => {
534                write!(f, "LN Unknown - variant={variant}")
535            }
536        }
537    }
538}
539
540#[derive(Debug)]
541pub struct LightningCommonInit;
542
543impl CommonModuleInit for LightningCommonInit {
544    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
545    const KIND: ModuleKind = KIND;
546
547    type ClientConfig = LightningClientConfig;
548
549    fn decoder() -> Decoder {
550        LightningModuleTypes::decoder()
551    }
552}
553
554pub struct LightningModuleTypes;
555
556plugin_types_trait_impl_common!(
557    KIND,
558    LightningModuleTypes,
559    LightningClientConfig,
560    LightningInput,
561    LightningOutput,
562    LightningOutputOutcome,
563    LightningConsensusItem,
564    LightningInputError,
565    LightningOutputError
566);
567
568// TODO: upstream serde support to LDK
569/// Hack to get a route hint that implements `serde` traits.
570pub mod route_hints {
571    #[cfg(feature = "uniffi")]
572    use std::str::FromStr;
573
574    use fedimint_core::encoding::{Decodable, Encodable};
575    use fedimint_core::secp256k1::PublicKey;
576    use lightning_invoice::RoutingFees;
577    use serde::{Deserialize, Serialize};
578
579    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
580    #[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
581    pub struct RouteHintHop {
582        /// The `node_id` of the non-target end of the route
583        pub src_node_id: PublicKey,
584        /// The `short_channel_id` of this channel
585        pub short_channel_id: u64,
586        /// Flat routing fee in millisatoshis
587        pub base_msat: u32,
588        /// Liquidity-based routing fee in millionths of a routed amount.
589        /// In other words, 10000 is 1%.
590        pub proportional_millionths: u32,
591        /// The difference in CLTV values between this node and the next node.
592        pub cltv_expiry_delta: u16,
593        /// The minimum value, in msat, which must be relayed to the next hop.
594        pub htlc_minimum_msat: Option<u64>,
595        /// The maximum value in msat available for routing with a single HTLC.
596        pub htlc_maximum_msat: Option<u64>,
597    }
598
599    #[cfg(feature = "uniffi")]
600    uniffi::custom_type!(PublicKey, String, {
601    remote,
602    lower: |pk| pk.to_string(),
603    try_lift: |s| PublicKey::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
604    });
605
606    /// A list of hops along a payment path terminating with a channel to the
607    /// recipient.
608    #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
609    pub struct RouteHint(pub Vec<RouteHintHop>);
610
611    #[cfg(feature = "uniffi")]
612    uniffi::custom_newtype!(RouteHint, Vec<RouteHintHop>);
613
614    impl RouteHint {
615        pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
616            lightning_invoice::RouteHint(
617                self.0
618                    .iter()
619                    .map(|hop| lightning_invoice::RouteHintHop {
620                        src_node_id: hop.src_node_id,
621                        short_channel_id: hop.short_channel_id,
622                        fees: RoutingFees {
623                            base_msat: hop.base_msat,
624                            proportional_millionths: hop.proportional_millionths,
625                        },
626                        cltv_expiry_delta: hop.cltv_expiry_delta,
627                        htlc_minimum_msat: hop.htlc_minimum_msat,
628                        htlc_maximum_msat: hop.htlc_maximum_msat,
629                    })
630                    .collect(),
631            )
632        }
633    }
634
635    impl From<lightning_invoice::RouteHint> for RouteHint {
636        fn from(rh: lightning_invoice::RouteHint) -> Self {
637            RouteHint(rh.0.into_iter().map(Into::into).collect())
638        }
639    }
640
641    impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
642        fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
643            RouteHintHop {
644                src_node_id: rhh.src_node_id,
645                short_channel_id: rhh.short_channel_id,
646                base_msat: rhh.fees.base_msat,
647                proportional_millionths: rhh.fees.proportional_millionths,
648                cltv_expiry_delta: rhh.cltv_expiry_delta,
649                htlc_minimum_msat: rhh.htlc_minimum_msat,
650                htlc_maximum_msat: rhh.htlc_maximum_msat,
651            }
652        }
653    }
654}
655
656// TODO: Upstream serde serialization for
657// lightning_invoice::RoutingFees
658// See https://github.com/lightningdevkit/rust-lightning/blob/b8ed4d2608e32128dd5a1dee92911638a4301138/lightning/src/routing/gossip.rs#L1057-L1065
659pub mod serde_routing_fees {
660    use lightning_invoice::RoutingFees;
661    use serde::ser::SerializeStruct;
662    use serde::{Deserialize, Deserializer, Serializer};
663
664    #[allow(missing_docs)]
665    pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
666    where
667        S: Serializer,
668    {
669        let mut state = serializer.serialize_struct("RoutingFees", 2)?;
670        state.serialize_field("base_msat", &fees.base_msat)?;
671        state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
672        state.end()
673    }
674
675    #[allow(missing_docs)]
676    pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
677    where
678        D: Deserializer<'de>,
679    {
680        let fees = serde_json::Value::deserialize(deserializer)?;
681        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
682        let base_msat = fees["base_msat"]
683            .as_u64()
684            .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
685        let proportional_millionths = fees["proportional_millionths"]
686            .as_u64()
687            .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
688
689        Ok(RoutingFees {
690            base_msat: base_msat
691                .try_into()
692                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
693            proportional_millionths: proportional_millionths.try_into().map_err(|_| {
694                serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
695            })?,
696        })
697    }
698}
699
700pub mod serde_option_routing_fees {
701    use lightning_invoice::RoutingFees;
702    use serde::ser::SerializeStruct;
703    use serde::{Deserialize, Deserializer, Serializer};
704
705    #[allow(missing_docs)]
706    pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
707    where
708        S: Serializer,
709    {
710        if let Some(fees) = fees {
711            let mut state = serializer.serialize_struct("RoutingFees", 2)?;
712            state.serialize_field("base_msat", &fees.base_msat)?;
713            state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
714            state.end()
715        } else {
716            let state = serializer.serialize_struct("RoutingFees", 0)?;
717            state.end()
718        }
719    }
720
721    #[allow(missing_docs)]
722    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
723    where
724        D: Deserializer<'de>,
725    {
726        let fees = serde_json::Value::deserialize(deserializer)?;
727        // While we deserialize fields as u64, RoutingFees expects u32 for the fields
728        let base_msat = fees["base_msat"].as_u64();
729
730        if let Some(base_msat) = base_msat
731            && let Some(proportional_millionths) = fees["proportional_millionths"].as_u64()
732        {
733            let base_msat: u32 = base_msat
734                .try_into()
735                .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
736            let proportional_millionths: u32 =
737                proportional_millionths.try_into().map_err(|_| {
738                    serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
739                })?;
740            return Ok(Some(RoutingFees {
741                base_msat,
742                proportional_millionths,
743            }));
744        }
745
746        Ok(None)
747    }
748}
749
750#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
751pub enum LightningInputError {
752    #[error("The input contract {0} does not exist")]
753    UnknownContract(ContractId),
754    #[error("The input contract has too little funds, got {0}, input spends {1}")]
755    InsufficientFunds(Amount, Amount),
756    #[error("An outgoing LN contract spend did not provide a preimage")]
757    MissingPreimage,
758    #[error("An outgoing LN contract spend provided a wrong preimage")]
759    InvalidPreimage,
760    #[error("Incoming contract not ready to be spent yet, decryption in progress")]
761    ContractNotReady,
762    #[error("The lightning input version is not supported by this federation")]
763    UnknownInputVariant(#[from] UnknownLightningInputVariantError),
764}
765
766#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
767pub enum LightningOutputError {
768    #[error("The input contract {0} does not exist")]
769    UnknownContract(ContractId),
770    #[error("Output contract value may not be zero unless it's an offer output")]
771    ZeroOutput,
772    #[error("Offer contains invalid threshold-encrypted data")]
773    InvalidEncryptedPreimage,
774    #[error("Offer contains a ciphertext that has already been used")]
775    DuplicateEncryptedPreimage,
776    #[error("The incoming LN account requires more funding (need {0} got {1})")]
777    InsufficientIncomingFunding(Amount, Amount),
778    #[error("No offer found for payment hash {0}")]
779    NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
780    #[error("Only outgoing contracts support cancellation")]
781    NotOutgoingContract,
782    #[error("Cancellation request wasn't properly signed")]
783    InvalidCancellationSignature,
784    #[error("The lightning output version is not supported by this federation")]
785    UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
786    // New variants have to be appended, the `Encodable` derive assigns indices by
787    // declaration order and clients decode this type from the API response.
788    #[error("The incoming contract {0} has already been funded")]
789    ContractAlreadyFunded(ContractId),
790    #[error("An incoming contract account for the offer's payment hash already exists: {0}")]
791    OfferForFundedContract(ContractId),
792    #[error("The contract's encrypted preimage does not match the offer's")]
793    EncryptedPreimageMismatch,
794    #[error("An incoming contract must be funded with a pending decryption")]
795    PreDecryptedIncomingContract,
796}
797
798/// Data needed to pay an invoice
799///
800/// This is a subset of the data from a [`lightning_invoice::Bolt11Invoice`]
801/// that does not contain the description, which increases privacy for the user.
802#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
803pub struct PrunedInvoice {
804    pub amount: Amount,
805    pub destination: secp256k1::PublicKey,
806    /// Wire-format encoding of feature bit vector
807    #[serde(with = "fedimint_core::hex::serde", default)]
808    pub destination_features: Vec<u8>,
809    pub payment_hash: sha256::Hash,
810    pub payment_secret: [u8; 32],
811    pub route_hints: Vec<RouteHint>,
812    pub min_final_cltv_delta: u64,
813    /// Time at which the invoice expires in seconds since unix epoch
814    pub expiry_timestamp: u64,
815}
816
817impl PrunedInvoice {
818    pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
819        // We use expires_at since it doesn't rely on the std feature in
820        // lightning-invoice. See #3838.
821        let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
822
823        let destination_features = if let Some(features) = invoice.features() {
824            encode_bolt11_invoice_features_without_length(features)
825        } else {
826            vec![]
827        };
828
829        PrunedInvoice {
830            amount,
831            destination: invoice
832                .payee_pub_key()
833                .copied()
834                .unwrap_or_else(|| invoice.recover_payee_pub_key()),
835            destination_features,
836            payment_hash: *invoice.payment_hash(),
837            payment_secret: invoice.payment_secret().0,
838            route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
839            min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
840            expiry_timestamp,
841        }
842    }
843}
844
845impl TryFrom<Bolt11Invoice> for PrunedInvoice {
846    type Error = anyhow::Error;
847
848    fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
849        Ok(PrunedInvoice::new(
850            &invoice,
851            Amount::from_msats(
852                invoice
853                    .amount_milli_satoshis()
854                    .context("Invoice amount is missing")?,
855            ),
856        ))
857    }
858}
859
860/// Request sent to the federation that requests the removal of a gateway
861/// registration. Each peer is expected to check the `signatures` map for the
862/// signature that validates the gateway authorized the removal of this
863/// registration.
864#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct RemoveGatewayRequest {
866    pub gateway_id: secp256k1::PublicKey,
867    pub signatures: BTreeMap<PeerId, Signature>,
868}
869
870/// Creates a message to be signed by the gateway's private key to prove that a
871/// registration was authorized by the holder of `info.gateway_id`. Message is
872/// defined as:
873///
874/// msg = sha256(tag + federation_public_key + nonce + info)
875///
876/// Tag is always `register-gateway`. Binding the federation public key stops a
877/// registration being replayed at a different federation, and the nonce stops
878/// an old registration being replayed at the same one.
879///
880/// Note this deliberately covers `info` but *not* the announcement's `ttl` or
881/// `vetted` flag. `ttl` is recomputed from `valid_until` every time a
882/// registration is read back ([`LightningGatewayRegistration::unanchor`]), so a
883/// signature over it would not survive being served to clients; it is bounded
884/// by [`MAX_GATEWAY_REGISTRATION_TTL`] instead. `vetted` is not the gateway's
885/// to assert, and guardians clear it on registration.
886pub fn create_gateway_registration_message(
887    federation_public_key: PublicKey,
888    nonce: u64,
889    info: &LightningGateway,
890) -> Message {
891    let mut message_preimage = "register-gateway".as_bytes().to_vec();
892    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
893    message_preimage.append(&mut nonce.consensus_encode_to_vec());
894    message_preimage.append(&mut info.consensus_encode_to_vec());
895    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
896}
897
898/// Creates a message to be signed by the Gateway's private key for the purpose
899/// of removing the gateway's registration record. Message is defined as:
900///
901/// msg = sha256(tag + federation_public_key + peer_id + challenge)
902///
903/// Tag is always `remove_gateway`. Challenge is unique for the registration
904/// record and acquired from each guardian prior to the removal request.
905pub fn create_gateway_remove_message(
906    federation_public_key: PublicKey,
907    peer_id: PeerId,
908    challenge: sha256::Hash,
909) -> Message {
910    let mut message_preimage = "remove-gateway".as_bytes().to_vec();
911    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
912    let guardian_id: u16 = peer_id.into();
913    message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
914    message_preimage.append(&mut challenge.consensus_encode_to_vec());
915    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
916}