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
8pub mod client;
18pub mod config;
19pub mod contracts;
20pub mod federation_endpoint_constants;
21pub mod gateway_endpoint_constants;
22mod preimage_auth;
23pub mod route_hints;
25pub mod serde_option_routing_fees;
26pub mod serde_routing_fees;
27#[cfg(test)]
28mod tests;
29
30pub const LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN: u16 = 30;
38
39pub 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;
71pub 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
84pub 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 pub amount: Amount,
123 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
178pub enum LightningOutputV0 {
179 Contract(ContractOutput),
181 Offer(contracts::incoming::IncomingContractOffer),
183 CancelOutgoing {
185 contract: ContractId,
187 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
302pub struct GatewayRegistrationAuth {
303 pub nonce: u64,
308 pub signature: Signature,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
318pub struct LightningGatewayRegistration {
319 pub info: LightningGateway,
320 pub vetted: bool,
322 pub valid_until: SystemTime,
325 #[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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
386pub struct LightningGatewayAnnouncement {
387 pub info: LightningGateway,
388 pub vetted: bool,
390 pub ttl: Duration,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub auth: Option<GatewayRegistrationAuth>,
398}
399
400pub const MAX_GATEWAY_REGISTRATION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
404
405impl LightningGatewayAnnouncement {
406 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 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#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
448pub struct LightningGateway {
449 #[serde(rename = "mint_channel_id")]
454 pub federation_index: u64,
455 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
457 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
458 pub lightning_alias: String,
459 pub api: SafeUrl,
462 pub route_hints: Vec<route_hints::RouteHint>,
467 #[serde(with = "serde_routing_fees")]
469 pub fees: RoutingFees,
470 pub gateway_id: secp256k1::PublicKey,
471 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 #[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#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
589pub struct PrunedInvoice {
590 pub amount: Amount,
591 pub destination: secp256k1::PublicKey,
592 #[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 pub expiry_timestamp: u64,
601}
602
603impl PrunedInvoice {
604 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
605 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct RemoveGatewayRequest {
658 pub gateway_id: secp256k1::PublicKey,
659 pub signatures: BTreeMap<PeerId, Signature>,
660}
661
662pub 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
690pub 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}