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