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;
25mod preimage_auth;
26
27/// Exclusive remaining-CLTV safety margin for funding LNv1 incoming contracts.
28///
29/// Fresh intercepted HTLCs must have more remaining blocks than this margin.
30/// The value matches the route-hint delta advertised by pre-upgrade clients so
31/// that their invoices remain payable; once the deployed client fleet
32/// advertises [`LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA`], enforcement can
33/// be raised towards it.
34pub const LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN: u16 = 30;
35
36/// Route-hint CLTV delta advertised in newly created LNv1 invoices.
37///
38/// Deliberately larger than the enforced
39/// [`LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN`]: new invoices reserve enough
40/// time for federation funding, threshold decryption, Lightning settlement,
41/// and on-chain recovery, so a future release can raise enforcement to this
42/// value without breaking payments.
43pub const LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA: u16 = 144;
44
45use std::collections::BTreeMap;
46use std::io::{Error, Read, Write};
47use std::time::{Duration, SystemTime};
48
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 preimage_auth::PreimageAuth;
63pub use reqwest::Method;
64use secp256k1::schnorr::Signature;
65use serde::{Deserialize, Serialize};
66use thiserror::Error;
67use threshold_crypto::PublicKey;
68/// The federation's aggregate public key, as committed to by a gateway
69/// registration proof. Re-exported so callers can verify proofs without
70/// depending on `threshold_crypto` directly.
71pub use threshold_crypto::PublicKey as FederationPublicKey;
72pub use {bitcoin, lightning_invoice};
73
74use crate::contracts::incoming::OfferId;
75use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
76use crate::route_hints::RouteHint;
77
78pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
79pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 1);
80
81/// From this module consensus version on, a contract account is funded exactly
82/// once and no offer can be created for a payment hash whose incoming contract
83/// account already exists.
84///
85/// Contract ids do not commit to the full contract state — an incoming
86/// contract's id is its payment hash and an outgoing contract's id omits the
87/// amount — so before this version a second funding output for the same id
88/// topped up the existing account while keeping its state. For an incoming
89/// contract whose preimage decryption already reached a terminal state, that
90/// let the first contract's gateway or preimage holder sweep the new funds.
91pub const CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
92    ModuleConsensusVersion::new(2, 1);
93
94extensible_associated_module_type!(
95    LightningInput,
96    LightningInputV0,
97    UnknownLightningInputVariantError
98);
99
100impl LightningInput {
101    pub fn new_v0(
102        contract_id: ContractId,
103        amount: Amount,
104        witness: Option<Preimage>,
105    ) -> LightningInput {
106        LightningInput::V0(LightningInputV0 {
107            contract_id,
108            amount,
109            witness,
110        })
111    }
112}
113
114#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
115pub struct LightningInputV0 {
116    pub contract_id: contracts::ContractId,
117    /// While for now we only support spending the entire contract we need to
118    /// avoid
119    pub amount: Amount,
120    /// Of the three contract types only the outgoing one needs any other
121    /// witness data than a signature. The signature is aggregated on the
122    /// transaction level, so only the optional preimage remains.
123    pub witness: Option<Preimage>,
124}
125
126impl std::fmt::Display for LightningInputV0 {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        write!(
129            f,
130            "Lightning Contract {} with amount {}",
131            self.contract_id, self.amount
132        )
133    }
134}
135
136extensible_associated_module_type!(
137    LightningOutput,
138    LightningOutputV0,
139    UnknownLightningOutputVariantError
140);
141
142impl LightningOutput {
143    pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
144        LightningOutput::V0(LightningOutputV0::Contract(contract))
145    }
146
147    pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
148        LightningOutput::V0(LightningOutputV0::Offer(offer))
149    }
150
151    pub fn new_v0_cancel_outgoing(
152        contract: ContractId,
153        gateway_signature: secp256k1::schnorr::Signature,
154    ) -> LightningOutput {
155        LightningOutput::V0(LightningOutputV0::CancelOutgoing {
156            contract,
157            gateway_signature,
158        })
159    }
160}
161
162/// Represents an output of the Lightning module.
163///
164/// There are three sub-types:
165///   * Normal contracts users may lock funds in
166///   * Offers to buy preimages (see `contracts::incoming` docs)
167///   * Early cancellation of outgoing contracts before their timeout
168///
169/// The offer type exists to register `IncomingContractOffer`s. Instead of
170/// patching in a second way of letting clients submit consensus items outside
171/// of transactions we let offers be a 0-amount output. We need to take care to
172/// allow 0-input, 1-output transactions for that to allow users to receive
173/// their first notes via LN without already having notes.
174#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
175pub enum LightningOutputV0 {
176    /// Fund contract
177    Contract(ContractOutput),
178    /// Create incoming contract offer
179    Offer(contracts::incoming::IncomingContractOffer),
180    /// Allow early refund of outgoing contract
181    CancelOutgoing {
182        /// Contract to update
183        contract: ContractId,
184        /// Signature of gateway
185        gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
186    },
187}
188
189impl std::fmt::Display for LightningOutputV0 {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        match self {
192            LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
193                Contract::Incoming(incoming) => {
194                    write!(
195                        f,
196                        "LN Incoming Contract for {} hash {}",
197                        amount, incoming.hash
198                    )
199                }
200                Contract::Outgoing(outgoing) => {
201                    write!(
202                        f,
203                        "LN Outgoing Contract for {} hash {}",
204                        amount, outgoing.hash
205                    )
206                }
207            },
208            LightningOutputV0::Offer(offer) => {
209                write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
210            }
211            LightningOutputV0::CancelOutgoing { contract, .. } => {
212                write!(f, "LN outgoing contract cancellation {contract}")
213            }
214        }
215    }
216}
217
218#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
219pub struct ContractOutput {
220    pub amount: fedimint_core::Amount,
221    pub contract: contracts::Contract,
222}
223
224#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
225pub struct ContractAccount {
226    pub amount: fedimint_core::Amount,
227    pub contract: contracts::FundedContract,
228}
229
230extensible_associated_module_type!(
231    LightningOutputOutcome,
232    LightningOutputOutcomeV0,
233    UnknownLightningOutputOutcomeVariantError
234);
235
236impl LightningOutputOutcome {
237    pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
238        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
239    }
240
241    pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
242        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
243    }
244
245    pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
246        LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
247    }
248}
249
250#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
251pub enum LightningOutputOutcomeV0 {
252    Contract {
253        id: ContractId,
254        outcome: ContractOutcome,
255    },
256    Offer {
257        id: OfferId,
258    },
259    CancelOutgoingContract {
260        id: ContractId,
261    },
262}
263
264impl LightningOutputOutcomeV0 {
265    pub fn is_permanent(&self) -> bool {
266        match self {
267            LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
268            LightningOutputOutcomeV0::Offer { .. }
269            | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
270        }
271    }
272}
273
274impl std::fmt::Display for LightningOutputOutcomeV0 {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self {
277            LightningOutputOutcomeV0::Contract { id, .. } => {
278                write!(f, "LN Contract {id}")
279            }
280            LightningOutputOutcomeV0::Offer { id } => {
281                write!(f, "LN Offer {id}")
282            }
283            LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
284                write!(f, "LN Outgoing Contract Cancellation {contract_id}")
285            }
286        }
287    }
288}
289
290/// Proof that a gateway registration was authorized by the holder of the
291/// secret key behind [`LightningGateway::gateway_id`].
292///
293/// Optional for backwards compatibility: gateways predating this field register
294/// unsigned, and guardians keep accepting them. A registration carrying a valid
295/// proof cannot be overwritten by an unsigned one, so a gateway becomes immune
296/// to identity hijacking as soon as it upgrades, without needing any other
297/// gateway, guardian or client to upgrade with it.
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
299#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
300pub struct GatewayRegistrationAuth {
301    /// Strictly increasing per gateway, so a captured signature cannot be
302    /// replayed to roll a registration back to stale settings. Guardians only
303    /// ever compare this against the value they already stored, never against
304    /// their own clock, so gateway and guardian clocks need not agree.
305    pub nonce: u64,
306    /// Schnorr signature over [`create_gateway_registration_message`].
307    pub signature: Signature,
308}
309
310/// Information about a gateway that is stored locally and expires based on
311/// local system time
312///
313/// Should only be serialized and deserialized in formats that can ignore
314/// additional fields as this struct may be extended in the future.
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
316pub struct LightningGatewayRegistration {
317    pub info: LightningGateway,
318    /// Indicates if this announcement has been vetted by the federation
319    pub vetted: bool,
320    /// Limits the validity of the announcement to allow updates, anchored to
321    /// local system time
322    pub valid_until: SystemTime,
323    /// Proof of possession of `info.gateway_id`, absent for registrations made
324    /// by gateways that predate it.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub auth: Option<GatewayRegistrationAuth>,
327}
328
329impl Encodable for LightningGatewayRegistration {
330    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
331        let json_repr = serde_json::to_string(self).map_err(|e| {
332            Error::other(format!(
333                "Failed to serialize LightningGatewayRegistration: {e}"
334            ))
335        })?;
336
337        json_repr.consensus_encode(writer)
338    }
339}
340
341impl Decodable for LightningGatewayRegistration {
342    fn consensus_decode_partial<R: Read>(
343        r: &mut R,
344        modules: &ModuleDecoderRegistry,
345    ) -> Result<Self, DecodeError> {
346        let json_repr = String::consensus_decode_partial(r, modules)?;
347        DecodeContext::context(
348            serde_json::from_str(&json_repr).map_err(DecodeError::from_err),
349            "Failed to deserialize LightningGatewayRegistration",
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(Into::into),
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(Into::into),
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
846/// A BOLT11 invoice carried no amount, so it cannot be pruned to the fields a
847/// gateway needs to pay it.
848#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
849#[error("The invoice does not specify an amount")]
850pub struct MissingInvoiceAmountError;
851
852impl TryFrom<Bolt11Invoice> for PrunedInvoice {
853    type Error = MissingInvoiceAmountError;
854
855    fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
856        Ok(PrunedInvoice::new(
857            &invoice,
858            Amount::from_msats(
859                invoice
860                    .amount_milli_satoshis()
861                    .ok_or(MissingInvoiceAmountError)?,
862            ),
863        ))
864    }
865}
866
867/// Request sent to the federation that requests the removal of a gateway
868/// registration. Each peer is expected to check the `signatures` map for the
869/// signature that validates the gateway authorized the removal of this
870/// registration.
871#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct RemoveGatewayRequest {
873    pub gateway_id: secp256k1::PublicKey,
874    pub signatures: BTreeMap<PeerId, Signature>,
875}
876
877/// Creates a message to be signed by the gateway's private key to prove that a
878/// registration was authorized by the holder of `info.gateway_id`. Message is
879/// defined as:
880///
881/// msg = sha256(tag + federation_public_key + nonce + info)
882///
883/// Tag is always `register-gateway`. Binding the federation public key stops a
884/// registration being replayed at a different federation, and the nonce stops
885/// an old registration being replayed at the same one.
886///
887/// Note this deliberately covers `info` but *not* the announcement's `ttl` or
888/// `vetted` flag. `ttl` is recomputed from `valid_until` every time a
889/// registration is read back ([`LightningGatewayRegistration::unanchor`]), so a
890/// signature over it would not survive being served to clients; it is bounded
891/// by [`MAX_GATEWAY_REGISTRATION_TTL`] instead. `vetted` is not the gateway's
892/// to assert, and guardians clear it on registration.
893pub fn create_gateway_registration_message(
894    federation_public_key: PublicKey,
895    nonce: u64,
896    info: &LightningGateway,
897) -> Message {
898    let mut message_preimage = "register-gateway".as_bytes().to_vec();
899    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
900    message_preimage.append(&mut nonce.consensus_encode_to_vec());
901    message_preimage.append(&mut info.consensus_encode_to_vec());
902    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
903}
904
905/// Creates a message to be signed by the Gateway's private key for the purpose
906/// of removing the gateway's registration record. Message is defined as:
907///
908/// msg = sha256(tag + federation_public_key + peer_id + challenge)
909///
910/// Tag is always `remove_gateway`. Challenge is unique for the registration
911/// record and acquired from each guardian prior to the removal request.
912pub fn create_gateway_remove_message(
913    federation_public_key: PublicKey,
914    peer_id: PeerId,
915    challenge: sha256::Hash,
916) -> Message {
917    let mut message_preimage = "remove-gateway".as_bytes().to_vec();
918    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
919    let guardian_id: u16 = peer_id.into();
920    message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
921    message_preimage.append(&mut challenge.consensus_encode_to_vec());
922    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
923}
924
925#[cfg(test)]
926mod tests {
927    use bitcoin::hashes::{Hash as _, sha256};
928    use bitcoin::secp256k1::{Secp256k1, SecretKey};
929    use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
930
931    use super::{MissingInvoiceAmountError, PrunedInvoice};
932
933    /// An invoice without an amount cannot be pruned, and says that rather
934    /// than returning a message.
935    #[test]
936    fn pruning_an_amountless_invoice_reports_the_missing_amount() {
937        let ctx = Secp256k1::new();
938        let sk = SecretKey::from_slice(&[1; 32]).expect("Valid secret key");
939        let invoice: Bolt11Invoice = InvoiceBuilder::new(Currency::Regtest)
940            .description(String::new())
941            .payment_hash(sha256::Hash::hash(&[0; 32]))
942            .current_timestamp()
943            .min_final_cltv_expiry_delta(0)
944            .payment_secret(PaymentSecret([0; 32]))
945            .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &sk))
946            .expect("Failed to build invoice");
947
948        assert_eq!(
949            PrunedInvoice::try_from(invoice),
950            Err(MissingInvoiceAmountError)
951        );
952    }
953}