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 config;
18pub mod contracts;
19pub mod federation_endpoint_constants;
20pub mod gateway_endpoint_constants;
21
22use std::collections::BTreeMap;
23use std::io::{Error, ErrorKind, 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 pub amount: Amount,
80 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
135pub enum LightningOutputV0 {
136 Contract(ContractOutput),
138 Offer(contracts::incoming::IncomingContractOffer),
140 CancelOutgoing {
142 contract: ContractId,
144 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
256pub struct LightningGatewayRegistration {
257 pub info: LightningGateway,
258 pub vetted: bool,
260 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::new(
269 ErrorKind::Other,
270 format!("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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
321pub struct LightningGatewayAnnouncement {
322 pub info: LightningGateway,
323 pub vetted: bool,
325 pub ttl: Duration,
329}
330
331impl LightningGatewayAnnouncement {
332 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#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
345pub struct LightningGateway {
346 #[serde(rename = "mint_channel_id")]
351 pub federation_index: u64,
352 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
354 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
355 pub lightning_alias: String,
356 pub api: SafeUrl,
359 pub route_hints: Vec<route_hints::RouteHint>,
364 #[serde(with = "serde_routing_fees")]
366 pub fees: RoutingFees,
367 pub gateway_id: secp256k1::PublicKey,
368 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 for contract {contract_id}")
388 }
389 LightningConsensusItem::BlockCount(count) => write!(f, "LN block count {count}"),
390 LightningConsensusItem::Default { variant, .. } => {
391 write!(f, "Unknown LN CI 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
425pub 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 pub src_node_id: PublicKey,
437 pub short_channel_id: u64,
439 pub base_msat: u32,
441 pub proportional_millionths: u32,
444 pub cltv_expiry_delta: u16,
446 pub htlc_minimum_msat: Option<u64>,
448 pub htlc_maximum_msat: Option<u64>,
450 }
451
452 #[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
499pub 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 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 let base_msat = fees["base_msat"].as_u64();
572
573 if let Some(base_msat) = base_msat {
574 if let Some(proportional_millionths) = fees["proportional_millionths"].as_u64() {
575 let base_msat: u32 = base_msat
576 .try_into()
577 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
578 let proportional_millionths: u32 =
579 proportional_millionths.try_into().map_err(|_| {
580 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
581 })?;
582 return Ok(Some(RoutingFees {
583 base_msat,
584 proportional_millionths,
585 }));
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#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
636pub struct PrunedInvoice {
637 pub amount: Amount,
638 pub destination: secp256k1::PublicKey,
639 #[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 pub expiry_timestamp: u64,
648}
649
650impl PrunedInvoice {
651 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
652 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#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct RemoveGatewayRequest {
699 pub gateway_id: secp256k1::PublicKey,
700 pub signatures: BTreeMap<PeerId, Signature>,
701}
702
703pub 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}