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, 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::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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
320pub struct LightningGatewayAnnouncement {
321 pub info: LightningGateway,
322 pub vetted: bool,
324 pub ttl: Duration,
328}
329
330impl LightningGatewayAnnouncement {
331 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#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
344pub struct LightningGateway {
345 #[serde(rename = "mint_channel_id")]
350 pub federation_index: u64,
351 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
353 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
354 pub lightning_alias: String,
355 pub api: SafeUrl,
358 pub route_hints: Vec<route_hints::RouteHint>,
363 #[serde(with = "serde_routing_fees")]
365 pub fees: RoutingFees,
366 pub gateway_id: secp256k1::PublicKey,
367 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
424pub 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 pub src_node_id: PublicKey,
436 pub short_channel_id: u64,
438 pub base_msat: u32,
440 pub proportional_millionths: u32,
443 pub cltv_expiry_delta: u16,
445 pub htlc_minimum_msat: Option<u64>,
447 pub htlc_maximum_msat: Option<u64>,
449 }
450
451 #[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
498pub 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 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 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
635pub struct PrunedInvoice {
636 pub amount: Amount,
637 pub destination: secp256k1::PublicKey,
638 #[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 pub expiry_timestamp: u64,
647}
648
649impl PrunedInvoice {
650 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
651 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#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct RemoveGatewayRequest {
698 pub gateway_id: secp256k1::PublicKey,
699 pub signatures: BTreeMap<PeerId, Signature>,
700}
701
702pub 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}