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;
22mod preimage_auth;
23/// Hack to get a route hint that implements `serde` traits.
24pub mod route_hints;
25pub mod serde_option_routing_fees;
26pub mod serde_routing_fees;
27#[cfg(test)]
28mod tests;
29
30/// Exclusive remaining-CLTV safety margin for funding LNv1 incoming contracts.
31///
32/// Fresh intercepted HTLCs must have more remaining blocks than this margin.
33/// The value matches the route-hint delta advertised by pre-upgrade clients so
34/// that their invoices remain payable; once the deployed client fleet
35/// advertises [`LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA`], enforcement can
36/// be raised towards it.
37pub const LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN: u16 = 30;
38
39/// Route-hint CLTV delta advertised in newly created LNv1 invoices.
40///
41/// Deliberately larger than the enforced
42/// [`LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN`]: new invoices reserve enough
43/// time for federation funding, threshold decryption, Lightning settlement,
44/// and on-chain recovery, so a future release can raise enforcement to this
45/// value without breaking payments.
46pub const LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA: u16 = 144;
47
48use std::collections::BTreeMap;
49use std::io::{Error, Read, Write};
50use std::time::{Duration, SystemTime};
51
52use bitcoin::hashes::{Hash, sha256};
53use config::LightningClientConfig;
54use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
55use fedimint_core::encoding::{Decodable, DecodeContext, DecodeError, Encodable};
56use fedimint_core::module::registry::ModuleDecoderRegistry;
57use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
58use fedimint_core::secp256k1::Message;
59use fedimint_core::util::SafeUrl;
60use fedimint_core::{
61    Amount, PeerId, encode_bolt11_invoice_features_without_length,
62    extensible_associated_module_type, plugin_types_trait_impl_common, secp256k1,
63};
64use lightning_invoice::{Bolt11Invoice, RoutingFees};
65pub use preimage_auth::PreimageAuth;
66pub use reqwest::Method;
67use secp256k1::schnorr::Signature;
68use serde::{Deserialize, Serialize};
69use thiserror::Error;
70use threshold_crypto::PublicKey;
71/// The federation's aggregate public key, as committed to by a gateway
72/// registration proof. Re-exported so callers can verify proofs without
73/// depending on `threshold_crypto` directly.
74pub use threshold_crypto::PublicKey as FederationPublicKey;
75pub use {bitcoin, lightning_invoice};
76
77use crate::contracts::incoming::OfferId;
78use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
79use crate::route_hints::RouteHint;
80
81pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
82pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 1);
83
84/// From this module consensus version on, a contract account is funded exactly
85/// once and no offer can be created for a payment hash whose incoming contract
86/// account already exists.
87///
88/// Contract ids do not commit to the full contract state — an incoming
89/// contract's id is its payment hash and an outgoing contract's id omits the
90/// amount — so before this version a second funding output for the same id
91/// topped up the existing account while keeping its state. For an incoming
92/// contract whose preimage decryption already reached a terminal state, that
93/// let the first contract's gateway or preimage holder sweep the new funds.
94pub const CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
95    ModuleConsensusVersion::new(2, 1);
96
97extensible_associated_module_type!(
98    LightningInput,
99    LightningInputV0,
100    UnknownLightningInputVariantError
101);
102
103impl LightningInput {
104    pub fn new_v0(
105        contract_id: ContractId,
106        amount: Amount,
107        witness: Option<Preimage>,
108    ) -> LightningInput {
109        LightningInput::V0(LightningInputV0 {
110            contract_id,
111            amount,
112            witness,
113        })
114    }
115}
116
117#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
118pub struct LightningInputV0 {
119    pub contract_id: contracts::ContractId,
120    /// While for now we only support spending the entire contract we need to
121    /// avoid
122    pub amount: Amount,
123    /// Of the three contract types only the outgoing one needs any other
124    /// witness data than a signature. The signature is aggregated on the
125    /// transaction level, so only the optional preimage remains.
126    pub witness: Option<Preimage>,
127}
128
129impl std::fmt::Display for LightningInputV0 {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(
132            f,
133            "Lightning Contract {} with amount {}",
134            self.contract_id, self.amount
135        )
136    }
137}
138
139extensible_associated_module_type!(
140    LightningOutput,
141    LightningOutputV0,
142    UnknownLightningOutputVariantError
143);
144
145impl LightningOutput {
146    pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
147        LightningOutput::V0(LightningOutputV0::Contract(contract))
148    }
149
150    pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
151        LightningOutput::V0(LightningOutputV0::Offer(offer))
152    }
153
154    pub fn new_v0_cancel_outgoing(
155        contract: ContractId,
156        gateway_signature: secp256k1::schnorr::Signature,
157    ) -> LightningOutput {
158        LightningOutput::V0(LightningOutputV0::CancelOutgoing {
159            contract,
160            gateway_signature,
161        })
162    }
163}
164
165/// Represents an output of the Lightning module.
166///
167/// There are three sub-types:
168///   * Normal contracts users may lock funds in
169///   * Offers to buy preimages (see `contracts::incoming` docs)
170///   * Early cancellation of outgoing contracts before their timeout
171///
172/// The offer type exists to register `IncomingContractOffer`s. Instead of
173/// patching in a second way of letting clients submit consensus items outside
174/// of transactions we let offers be a 0-amount output. We need to take care to
175/// allow 0-input, 1-output transactions for that to allow users to receive
176/// their first notes via LN without already having notes.
177#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
178pub enum LightningOutputV0 {
179    /// Fund contract
180    Contract(ContractOutput),
181    /// Create incoming contract offer
182    Offer(contracts::incoming::IncomingContractOffer),
183    /// Allow early refund of outgoing contract
184    CancelOutgoing {
185        /// Contract to update
186        contract: ContractId,
187        /// Signature of gateway
188        gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
189    },
190}
191
192impl std::fmt::Display for LightningOutputV0 {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        match self {
195            LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
196                Contract::Incoming(incoming) => {
197                    write!(
198                        f,
199                        "LN Incoming Contract for {} hash {}",
200                        amount, incoming.hash
201                    )
202                }
203                Contract::Outgoing(outgoing) => {
204                    write!(
205                        f,
206                        "LN Outgoing Contract for {} hash {}",
207                        amount, outgoing.hash
208                    )
209                }
210            },
211            LightningOutputV0::Offer(offer) => {
212                write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
213            }
214            LightningOutputV0::CancelOutgoing { contract, .. } => {
215                write!(f, "LN outgoing contract cancellation {contract}")
216            }
217        }
218    }
219}
220
221#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
222pub struct ContractOutput {
223    pub amount: fedimint_core::Amount,
224    pub contract: contracts::Contract,
225}
226
227#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
228pub struct ContractAccount {
229    pub amount: fedimint_core::Amount,
230    pub contract: contracts::FundedContract,
231}
232
233extensible_associated_module_type!(
234    LightningOutputOutcome,
235    LightningOutputOutcomeV0,
236    UnknownLightningOutputOutcomeVariantError
237);
238
239impl LightningOutputOutcome {
240    pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
241        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
242    }
243
244    pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
245        LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
246    }
247
248    pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
249        LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
250    }
251}
252
253#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
254pub enum LightningOutputOutcomeV0 {
255    Contract {
256        id: ContractId,
257        outcome: ContractOutcome,
258    },
259    Offer {
260        id: OfferId,
261    },
262    CancelOutgoingContract {
263        id: ContractId,
264    },
265}
266
267impl LightningOutputOutcomeV0 {
268    pub fn is_permanent(&self) -> bool {
269        match self {
270            LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
271            LightningOutputOutcomeV0::Offer { .. }
272            | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
273        }
274    }
275}
276
277impl std::fmt::Display for LightningOutputOutcomeV0 {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        match self {
280            LightningOutputOutcomeV0::Contract { id, .. } => {
281                write!(f, "LN Contract {id}")
282            }
283            LightningOutputOutcomeV0::Offer { id } => {
284                write!(f, "LN Offer {id}")
285            }
286            LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
287                write!(f, "LN Outgoing Contract Cancellation {contract_id}")
288            }
289        }
290    }
291}
292
293/// Proof that a gateway registration was authorized by the holder of the
294/// secret key behind [`LightningGateway::gateway_id`].
295///
296/// Optional for backwards compatibility: gateways predating this field register
297/// unsigned, and guardians keep accepting them. A registration carrying a valid
298/// proof cannot be overwritten by an unsigned one, so a gateway becomes immune
299/// to identity hijacking as soon as it upgrades, without needing any other
300/// gateway, guardian or client to upgrade with it.
301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
302pub struct GatewayRegistrationAuth {
303    /// Strictly increasing per gateway, so a captured signature cannot be
304    /// replayed to roll a registration back to stale settings. Guardians only
305    /// ever compare this against the value they already stored, never against
306    /// their own clock, so gateway and guardian clocks need not agree.
307    pub nonce: u64,
308    /// Schnorr signature over [`create_gateway_registration_message`].
309    pub signature: Signature,
310}
311
312/// Information about a gateway that is stored locally and expires based on
313/// local system time
314///
315/// Should only be serialized and deserialized in formats that can ignore
316/// additional fields as this struct may be extended in the future.
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
318pub struct LightningGatewayRegistration {
319    pub info: LightningGateway,
320    /// Indicates if this announcement has been vetted by the federation
321    pub vetted: bool,
322    /// Limits the validity of the announcement to allow updates, anchored to
323    /// local system time
324    pub valid_until: SystemTime,
325    /// Proof of possession of `info.gateway_id`, absent for registrations made
326    /// by gateways that predate it.
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub auth: Option<GatewayRegistrationAuth>,
329}
330
331impl Encodable for LightningGatewayRegistration {
332    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
333        let json_repr = serde_json::to_string(self).map_err(|e| {
334            Error::other(format!(
335                "Failed to serialize LightningGatewayRegistration: {e}"
336            ))
337        })?;
338
339        json_repr.consensus_encode(writer)
340    }
341}
342
343impl Decodable for LightningGatewayRegistration {
344    fn consensus_decode_partial<R: Read>(
345        r: &mut R,
346        modules: &ModuleDecoderRegistry,
347    ) -> Result<Self, DecodeError> {
348        let json_repr = String::consensus_decode_partial(r, modules)?;
349        DecodeContext::context(
350            serde_json::from_str(&json_repr).map_err(DecodeError::from_err),
351            "Failed to deserialize LightningGatewayRegistration",
352        )
353    }
354}
355
356impl LightningGatewayRegistration {
357    /// Create an announcement from this registration that is ttl-limited by
358    /// a floating duration. This is useful for sharing the announcement with
359    /// other nodes with unsynchronized clocks which can then anchor the
360    /// announcement to their local system time.
361    pub fn unanchor(self) -> LightningGatewayAnnouncement {
362        LightningGatewayAnnouncement {
363            info: self.info,
364            ttl: self
365                .valid_until
366                .duration_since(fedimint_core::time::now())
367                .unwrap_or_default(),
368            vetted: self.vetted,
369            auth: self.auth,
370        }
371    }
372
373    pub fn is_expired(&self) -> bool {
374        self.valid_until < fedimint_core::time::now()
375    }
376}
377
378/// Information about a gateway that is shared with other federation members and
379/// expires based on a TTL to allow for sharing between nodes with
380/// unsynchronized clocks which can each anchor the announcement to their local
381/// system time.
382///
383/// Should only be serialized and deserialized in formats that can ignore
384/// additional fields as this struct may be extended in the future.
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
386pub struct LightningGatewayAnnouncement {
387    pub info: LightningGateway,
388    /// Indicates if this announcement has been vetted by the federation
389    pub vetted: bool,
390    /// Limits the validity of the announcement to allow updates, unanchored to
391    /// local system time to allow sharing between nodes with unsynchronized
392    /// clocks
393    pub ttl: Duration,
394    /// Proof of possession of `info.gateway_id`, absent for gateways that
395    /// predate it.
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub auth: Option<GatewayRegistrationAuth>,
398}
399
400/// Upper bound guardians place on a registration's lifetime. Gateways announce
401/// a TTL two orders of magnitude below this, so it only ever binds on
402/// announcements that are trying to squat a `gateway_id` indefinitely.
403pub const MAX_GATEWAY_REGISTRATION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
404
405impl LightningGatewayAnnouncement {
406    /// Whether this announcement's proof of possession holds, treating an
407    /// announcement without one as valid — unsigned registrations are still
408    /// accepted, they simply earn no preference.
409    ///
410    /// Callers must apply this *before* preferring signed announcements over
411    /// unsigned ones. A proof that is merely present is worthless: anyone can
412    /// attach a garbage signature to someone else's `gateway_id`, so preferring
413    /// on presence alone lets a single peer evict every honest unsigned
414    /// announcement for a gateway.
415    pub fn registration_proof_is_valid(&self, federation_public_key: PublicKey) -> bool {
416        let Some(auth) = &self.auth else {
417            return true;
418        };
419
420        let msg =
421            create_gateway_registration_message(federation_public_key, auth.nonce, &self.info);
422
423        auth.signature
424            .verify(&msg, &self.info.gateway_id.x_only_public_key().0)
425            .is_ok()
426    }
427
428    /// Create a registration from this announcement that is anchored to the
429    /// local system time.
430    ///
431    /// The TTL is clamped to [`MAX_GATEWAY_REGISTRATION_TTL`], which also keeps
432    /// the addition below from having to handle an attacker-supplied
433    /// [`Duration`] large enough to overflow [`SystemTime`].
434    pub fn anchor(self) -> LightningGatewayRegistration {
435        let ttl = self.ttl.min(MAX_GATEWAY_REGISTRATION_TTL);
436
437        LightningGatewayRegistration {
438            info: self.info,
439            vetted: self.vetted,
440            valid_until: fedimint_core::time::now() + ttl,
441            auth: self.auth,
442        }
443    }
444}
445
446/// Information a gateway registers with a federation
447#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
448pub struct LightningGateway {
449    /// Unique per-federation identifier assigned by the gateway.
450    /// All clients in this federation should use this value as
451    /// `short_channel_id` when creating invoices to be settled by this
452    /// gateway.
453    #[serde(rename = "mint_channel_id")]
454    pub federation_index: u64,
455    /// Key used to pay the gateway
456    pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
457    pub node_pub_key: fedimint_core::secp256k1::PublicKey,
458    pub lightning_alias: String,
459    /// URL to the gateway's versioned public API
460    /// (e.g. <https://gateway.example.com/v1>)
461    pub api: SafeUrl,
462    /// Route hints to reach the LN node of the gateway.
463    ///
464    /// These will be appended with the route hint of the recipient's virtual
465    /// channel. To keeps invoices small these should be used sparingly.
466    pub route_hints: Vec<route_hints::RouteHint>,
467    /// Gateway configured routing fees
468    #[serde(with = "serde_routing_fees")]
469    pub fees: RoutingFees,
470    pub gateway_id: secp256k1::PublicKey,
471    /// Indicates if the gateway supports private payments
472    pub supports_private_payments: bool,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
476pub enum LightningConsensusItem {
477    DecryptPreimage(ContractId, PreimageDecryptionShare),
478    BlockCount(u64),
479    ModuleConsensusVersion(ModuleConsensusVersion),
480    #[encodable_default]
481    Default {
482        variant: u64,
483        bytes: Vec<u8>,
484    },
485}
486
487impl std::fmt::Display for LightningConsensusItem {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        match self {
490            LightningConsensusItem::DecryptPreimage(contract_id, _) => {
491                write!(f, "LN Decryption Share - contract_id: {contract_id}")
492            }
493            LightningConsensusItem::BlockCount(count) => write!(f, "LN Block Count {count}"),
494            LightningConsensusItem::ModuleConsensusVersion(version) => {
495                write!(
496                    f,
497                    "LN Consensus Version {}.{}",
498                    version.major, version.minor
499                )
500            }
501            LightningConsensusItem::Default { variant, .. } => {
502                write!(f, "LN Unknown - variant={variant}")
503            }
504        }
505    }
506}
507
508#[derive(Debug)]
509pub struct LightningCommonInit;
510
511impl CommonModuleInit for LightningCommonInit {
512    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
513    const KIND: ModuleKind = KIND;
514
515    type ClientConfig = LightningClientConfig;
516
517    fn decoder() -> Decoder {
518        LightningModuleTypes::decoder()
519    }
520}
521
522pub struct LightningModuleTypes;
523
524plugin_types_trait_impl_common!(
525    KIND,
526    LightningModuleTypes,
527    LightningClientConfig,
528    LightningInput,
529    LightningOutput,
530    LightningOutputOutcome,
531    LightningConsensusItem,
532    LightningInputError,
533    LightningOutputError
534);
535
536#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
537pub enum LightningInputError {
538    #[error("The input contract {0} does not exist")]
539    UnknownContract(ContractId),
540    #[error("The input contract has too little funds, got {0}, input spends {1}")]
541    InsufficientFunds(Amount, Amount),
542    #[error("An outgoing LN contract spend did not provide a preimage")]
543    MissingPreimage,
544    #[error("An outgoing LN contract spend provided a wrong preimage")]
545    InvalidPreimage,
546    #[error("Incoming contract not ready to be spent yet, decryption in progress")]
547    ContractNotReady,
548    #[error("The lightning input version is not supported by this federation")]
549    UnknownInputVariant(#[from] UnknownLightningInputVariantError),
550}
551
552#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
553pub enum LightningOutputError {
554    #[error("The input contract {0} does not exist")]
555    UnknownContract(ContractId),
556    #[error("Output contract value may not be zero unless it's an offer output")]
557    ZeroOutput,
558    #[error("Offer contains invalid threshold-encrypted data")]
559    InvalidEncryptedPreimage,
560    #[error("Offer contains a ciphertext that has already been used")]
561    DuplicateEncryptedPreimage,
562    #[error("The incoming LN account requires more funding (need {0} got {1})")]
563    InsufficientIncomingFunding(Amount, Amount),
564    #[error("No offer found for payment hash {0}")]
565    NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
566    #[error("Only outgoing contracts support cancellation")]
567    NotOutgoingContract,
568    #[error("Cancellation request wasn't properly signed")]
569    InvalidCancellationSignature,
570    #[error("The lightning output version is not supported by this federation")]
571    UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
572    // New variants have to be appended, the `Encodable` derive assigns indices by
573    // declaration order and clients decode this type from the API response.
574    #[error("The incoming contract {0} has already been funded")]
575    ContractAlreadyFunded(ContractId),
576    #[error("An incoming contract account for the offer's payment hash already exists: {0}")]
577    OfferForFundedContract(ContractId),
578    #[error("The contract's encrypted preimage does not match the offer's")]
579    EncryptedPreimageMismatch,
580    #[error("An incoming contract must be funded with a pending decryption")]
581    PreDecryptedIncomingContract,
582}
583
584/// Data needed to pay an invoice
585///
586/// This is a subset of the data from a [`lightning_invoice::Bolt11Invoice`]
587/// that does not contain the description, which increases privacy for the user.
588#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
589pub struct PrunedInvoice {
590    pub amount: Amount,
591    pub destination: secp256k1::PublicKey,
592    /// Wire-format encoding of feature bit vector
593    #[serde(with = "fedimint_core::hex::serde", default)]
594    pub destination_features: Vec<u8>,
595    pub payment_hash: sha256::Hash,
596    pub payment_secret: [u8; 32],
597    pub route_hints: Vec<RouteHint>,
598    pub min_final_cltv_delta: u64,
599    /// Time at which the invoice expires in seconds since unix epoch
600    pub expiry_timestamp: u64,
601}
602
603impl PrunedInvoice {
604    pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
605        // We use expires_at since it doesn't rely on the std feature in
606        // lightning-invoice. See #3838.
607        let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
608
609        let destination_features = if let Some(features) = invoice.features() {
610            encode_bolt11_invoice_features_without_length(features)
611        } else {
612            vec![]
613        };
614
615        PrunedInvoice {
616            amount,
617            destination: invoice
618                .payee_pub_key()
619                .copied()
620                .unwrap_or_else(|| invoice.recover_payee_pub_key()),
621            destination_features,
622            payment_hash: *invoice.payment_hash(),
623            payment_secret: invoice.payment_secret().0,
624            route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
625            min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
626            expiry_timestamp,
627        }
628    }
629}
630
631/// A BOLT11 invoice carried no amount, so it cannot be pruned to the fields a
632/// gateway needs to pay it.
633#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
634#[error("The invoice does not specify an amount")]
635pub struct MissingInvoiceAmountError;
636
637impl TryFrom<Bolt11Invoice> for PrunedInvoice {
638    type Error = MissingInvoiceAmountError;
639
640    fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
641        Ok(PrunedInvoice::new(
642            &invoice,
643            Amount::from_msats(
644                invoice
645                    .amount_milli_satoshis()
646                    .ok_or(MissingInvoiceAmountError)?,
647            ),
648        ))
649    }
650}
651
652/// Request sent to the federation that requests the removal of a gateway
653/// registration. Each peer is expected to check the `signatures` map for the
654/// signature that validates the gateway authorized the removal of this
655/// registration.
656#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct RemoveGatewayRequest {
658    pub gateway_id: secp256k1::PublicKey,
659    pub signatures: BTreeMap<PeerId, Signature>,
660}
661
662/// Creates a message to be signed by the gateway's private key to prove that a
663/// registration was authorized by the holder of `info.gateway_id`. Message is
664/// defined as:
665///
666/// msg = sha256(tag + federation_public_key + nonce + info)
667///
668/// Tag is always `register-gateway`. Binding the federation public key stops a
669/// registration being replayed at a different federation, and the nonce stops
670/// an old registration being replayed at the same one.
671///
672/// Note this deliberately covers `info` but *not* the announcement's `ttl` or
673/// `vetted` flag. `ttl` is recomputed from `valid_until` every time a
674/// registration is read back ([`LightningGatewayRegistration::unanchor`]), so a
675/// signature over it would not survive being served to clients; it is bounded
676/// by [`MAX_GATEWAY_REGISTRATION_TTL`] instead. `vetted` is not the gateway's
677/// to assert, and guardians clear it on registration.
678pub fn create_gateway_registration_message(
679    federation_public_key: PublicKey,
680    nonce: u64,
681    info: &LightningGateway,
682) -> Message {
683    let mut message_preimage = "register-gateway".as_bytes().to_vec();
684    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
685    message_preimage.append(&mut nonce.consensus_encode_to_vec());
686    message_preimage.append(&mut info.consensus_encode_to_vec());
687    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
688}
689
690/// Creates a message to be signed by the Gateway's private key for the purpose
691/// of removing the gateway's registration record. Message is defined as:
692///
693/// msg = sha256(tag + federation_public_key + peer_id + challenge)
694///
695/// Tag is always `remove_gateway`. Challenge is unique for the registration
696/// record and acquired from each guardian prior to the removal request.
697pub fn create_gateway_remove_message(
698    federation_public_key: PublicKey,
699    peer_id: PeerId,
700    challenge: sha256::Hash,
701) -> Message {
702    let mut message_preimage = "remove-gateway".as_bytes().to_vec();
703    message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
704    let guardian_id: u16 = peer_id.into();
705    message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
706    message_preimage.append(&mut challenge.consensus_encode_to_vec());
707    Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
708}