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