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