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
8extern crate core;
18
19pub mod config;
20pub mod contracts;
21pub mod federation_endpoint_constants;
22pub mod gateway_endpoint_constants;
23
24use std::collections::BTreeMap;
25use std::io::{Error, ErrorKind, Read, Write};
26use std::time::{Duration, SystemTime};
27
28use anyhow::{bail, Context as AnyhowContext};
29use bitcoin::hashes::{sha256, Hash};
30use config::LightningClientConfig;
31use fedimint_client::oplog::OperationLogEntry;
32use fedimint_client::ClientHandleArc;
33use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind, OperationId};
34use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
35use fedimint_core::module::registry::ModuleDecoderRegistry;
36use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
37use fedimint_core::secp256k1::Message;
38use fedimint_core::util::SafeUrl;
39use fedimint_core::{
40 encode_bolt11_invoice_features_without_length, extensible_associated_module_type,
41 plugin_types_trait_impl_common, secp256k1, Amount, PeerId,
42};
43use lightning_invoice::{Bolt11Invoice, RoutingFees};
44use secp256k1::schnorr::Signature;
45use serde::{Deserialize, Serialize};
46use thiserror::Error;
47use threshold_crypto::PublicKey;
48use tracing::error;
49pub use {bitcoin, lightning_invoice};
50
51use crate::contracts::incoming::OfferId;
52use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
53use crate::route_hints::RouteHint;
54
55pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
56pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 0);
57
58extensible_associated_module_type!(
59 LightningInput,
60 LightningInputV0,
61 UnknownLightningInputVariantError
62);
63
64impl LightningInput {
65 pub fn new_v0(
66 contract_id: ContractId,
67 amount: Amount,
68 witness: Option<Preimage>,
69 ) -> LightningInput {
70 LightningInput::V0(LightningInputV0 {
71 contract_id,
72 amount,
73 witness,
74 })
75 }
76}
77
78#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
79pub struct LightningInputV0 {
80 pub contract_id: contracts::ContractId,
81 pub amount: Amount,
84 pub witness: Option<Preimage>,
88}
89
90impl std::fmt::Display for LightningInputV0 {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(
93 f,
94 "Lightning Contract {} with amount {}",
95 self.contract_id, self.amount
96 )
97 }
98}
99
100extensible_associated_module_type!(
101 LightningOutput,
102 LightningOutputV0,
103 UnknownLightningOutputVariantError
104);
105
106impl LightningOutput {
107 pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
108 LightningOutput::V0(LightningOutputV0::Contract(contract))
109 }
110
111 pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
112 LightningOutput::V0(LightningOutputV0::Offer(offer))
113 }
114
115 pub fn new_v0_cancel_outgoing(
116 contract: ContractId,
117 gateway_signature: secp256k1::schnorr::Signature,
118 ) -> LightningOutput {
119 LightningOutput::V0(LightningOutputV0::CancelOutgoing {
120 contract,
121 gateway_signature,
122 })
123 }
124}
125
126#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
139pub enum LightningOutputV0 {
140 Contract(ContractOutput),
142 Offer(contracts::incoming::IncomingContractOffer),
144 CancelOutgoing {
146 contract: ContractId,
148 gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
150 },
151}
152
153impl std::fmt::Display for LightningOutputV0 {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
157 Contract::Incoming(incoming) => {
158 write!(
159 f,
160 "LN Incoming Contract for {} hash {}",
161 amount, incoming.hash
162 )
163 }
164 Contract::Outgoing(outgoing) => {
165 write!(
166 f,
167 "LN Outgoing Contract for {} hash {}",
168 amount, outgoing.hash
169 )
170 }
171 },
172 LightningOutputV0::Offer(offer) => {
173 write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
174 }
175 LightningOutputV0::CancelOutgoing { contract, .. } => {
176 write!(f, "LN outgoing contract cancellation {contract}")
177 }
178 }
179 }
180}
181
182#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
183pub struct ContractOutput {
184 pub amount: fedimint_core::Amount,
185 pub contract: contracts::Contract,
186}
187
188#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
189pub struct ContractAccount {
190 pub amount: fedimint_core::Amount,
191 pub contract: contracts::FundedContract,
192}
193
194extensible_associated_module_type!(
195 LightningOutputOutcome,
196 LightningOutputOutcomeV0,
197 UnknownLightningOutputOutcomeVariantError
198);
199
200impl LightningOutputOutcome {
201 pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
202 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
203 }
204
205 pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
206 LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
207 }
208
209 pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
210 LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
211 }
212}
213
214#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
215pub enum LightningOutputOutcomeV0 {
216 Contract {
217 id: ContractId,
218 outcome: ContractOutcome,
219 },
220 Offer {
221 id: OfferId,
222 },
223 CancelOutgoingContract {
224 id: ContractId,
225 },
226}
227
228impl LightningOutputOutcomeV0 {
229 pub fn is_permanent(&self) -> bool {
230 match self {
231 LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
232 LightningOutputOutcomeV0::Offer { .. }
233 | LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
234 }
235 }
236}
237
238impl std::fmt::Display for LightningOutputOutcomeV0 {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 match self {
241 LightningOutputOutcomeV0::Contract { id, .. } => {
242 write!(f, "LN Contract {id}")
243 }
244 LightningOutputOutcomeV0::Offer { id } => {
245 write!(f, "LN Offer {id}")
246 }
247 LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
248 write!(f, "LN Outgoing Contract Cancellation {contract_id}")
249 }
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
260pub struct LightningGatewayRegistration {
261 pub info: LightningGateway,
262 pub vetted: bool,
264 pub valid_until: SystemTime,
267}
268
269impl Encodable for LightningGatewayRegistration {
270 fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
271 let json_repr = serde_json::to_string(self).map_err(|e| {
272 Error::new(
273 ErrorKind::Other,
274 format!("Failed to serialize LightningGatewayRegistration: {e}"),
275 )
276 })?;
277
278 json_repr.consensus_encode(writer)
279 }
280}
281
282impl Decodable for LightningGatewayRegistration {
283 fn consensus_decode<R: Read>(
284 r: &mut R,
285 modules: &ModuleDecoderRegistry,
286 ) -> Result<Self, DecodeError> {
287 let json_repr = String::consensus_decode(r, modules)?;
288 serde_json::from_str(&json_repr).map_err(|e| {
289 DecodeError::new_custom(
290 anyhow::Error::new(e).context("Failed to deserialize LightningGatewayRegistration"),
291 )
292 })
293 }
294}
295
296impl LightningGatewayRegistration {
297 pub fn unanchor(self) -> LightningGatewayAnnouncement {
302 LightningGatewayAnnouncement {
303 info: self.info,
304 ttl: self
305 .valid_until
306 .duration_since(fedimint_core::time::now())
307 .unwrap_or_default(),
308 vetted: self.vetted,
309 }
310 }
311
312 pub fn is_expired(&self) -> bool {
313 self.valid_until < fedimint_core::time::now()
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
325pub struct LightningGatewayAnnouncement {
326 pub info: LightningGateway,
327 pub vetted: bool,
329 pub ttl: Duration,
333}
334
335impl LightningGatewayAnnouncement {
336 pub fn anchor(self) -> LightningGatewayRegistration {
339 LightningGatewayRegistration {
340 info: self.info,
341 vetted: self.vetted,
342 valid_until: fedimint_core::time::now() + self.ttl,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
349pub struct LightningGateway {
350 #[serde(rename = "mint_channel_id")]
355 pub federation_index: u64,
356 pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
358 pub node_pub_key: fedimint_core::secp256k1::PublicKey,
359 pub lightning_alias: String,
360 pub api: SafeUrl,
363 pub route_hints: Vec<route_hints::RouteHint>,
368 #[serde(with = "serde_routing_fees")]
370 pub fees: RoutingFees,
371 pub gateway_id: secp256k1::PublicKey,
372 pub supports_private_payments: bool,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
377pub enum LightningConsensusItem {
378 DecryptPreimage(ContractId, PreimageDecryptionShare),
379 BlockCount(u64),
380 #[encodable_default]
381 Default {
382 variant: u64,
383 bytes: Vec<u8>,
384 },
385}
386
387impl std::fmt::Display for LightningConsensusItem {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 match self {
390 LightningConsensusItem::DecryptPreimage(contract_id, _) => {
391 write!(f, "LN Decryption Share for contract {contract_id}")
392 }
393 LightningConsensusItem::BlockCount(count) => write!(f, "LN block count {count}"),
394 LightningConsensusItem::Default { variant, .. } => {
395 write!(f, "Unknown LN CI variant={variant}")
396 }
397 }
398 }
399}
400
401#[derive(Debug)]
402pub struct LightningCommonInit;
403
404impl CommonModuleInit for LightningCommonInit {
405 const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
406 const KIND: ModuleKind = KIND;
407
408 type ClientConfig = LightningClientConfig;
409
410 fn decoder() -> Decoder {
411 LightningModuleTypes::decoder()
412 }
413}
414
415pub struct LightningModuleTypes;
416
417plugin_types_trait_impl_common!(
418 KIND,
419 LightningModuleTypes,
420 LightningClientConfig,
421 LightningInput,
422 LightningOutput,
423 LightningOutputOutcome,
424 LightningConsensusItem,
425 LightningInputError,
426 LightningOutputError
427);
428
429pub mod route_hints {
432 use fedimint_core::encoding::{Decodable, Encodable};
433 use fedimint_core::secp256k1::PublicKey;
434 use lightning_invoice::RoutingFees;
435 use serde::{Deserialize, Serialize};
436
437 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
438 pub struct RouteHintHop {
439 pub src_node_id: PublicKey,
441 pub short_channel_id: u64,
443 pub base_msat: u32,
445 pub proportional_millionths: u32,
448 pub cltv_expiry_delta: u16,
450 pub htlc_minimum_msat: Option<u64>,
452 pub htlc_maximum_msat: Option<u64>,
454 }
455
456 #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
459 pub struct RouteHint(pub Vec<RouteHintHop>);
460
461 impl RouteHint {
462 pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
463 lightning_invoice::RouteHint(
464 self.0
465 .iter()
466 .map(|hop| lightning_invoice::RouteHintHop {
467 src_node_id: hop.src_node_id,
468 short_channel_id: hop.short_channel_id,
469 fees: RoutingFees {
470 base_msat: hop.base_msat,
471 proportional_millionths: hop.proportional_millionths,
472 },
473 cltv_expiry_delta: hop.cltv_expiry_delta,
474 htlc_minimum_msat: hop.htlc_minimum_msat,
475 htlc_maximum_msat: hop.htlc_maximum_msat,
476 })
477 .collect(),
478 )
479 }
480 }
481
482 impl From<lightning_invoice::RouteHint> for RouteHint {
483 fn from(rh: lightning_invoice::RouteHint) -> Self {
484 RouteHint(rh.0.into_iter().map(Into::into).collect())
485 }
486 }
487
488 impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
489 fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
490 RouteHintHop {
491 src_node_id: rhh.src_node_id,
492 short_channel_id: rhh.short_channel_id,
493 base_msat: rhh.fees.base_msat,
494 proportional_millionths: rhh.fees.proportional_millionths,
495 cltv_expiry_delta: rhh.cltv_expiry_delta,
496 htlc_minimum_msat: rhh.htlc_minimum_msat,
497 htlc_maximum_msat: rhh.htlc_maximum_msat,
498 }
499 }
500 }
501}
502
503pub mod serde_routing_fees {
507 use lightning_invoice::RoutingFees;
508 use serde::ser::SerializeStruct;
509 use serde::{Deserialize, Deserializer, Serializer};
510
511 #[allow(missing_docs)]
512 pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
513 where
514 S: Serializer,
515 {
516 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
517 state.serialize_field("base_msat", &fees.base_msat)?;
518 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
519 state.end()
520 }
521
522 #[allow(missing_docs)]
523 pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
524 where
525 D: Deserializer<'de>,
526 {
527 let fees = serde_json::Value::deserialize(deserializer)?;
528 let base_msat = fees["base_msat"]
530 .as_u64()
531 .ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
532 let proportional_millionths = fees["proportional_millionths"]
533 .as_u64()
534 .ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
535
536 Ok(RoutingFees {
537 base_msat: base_msat
538 .try_into()
539 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
540 proportional_millionths: proportional_millionths.try_into().map_err(|_| {
541 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
542 })?,
543 })
544 }
545}
546
547pub mod serde_option_routing_fees {
548 use lightning_invoice::RoutingFees;
549 use serde::ser::SerializeStruct;
550 use serde::{Deserialize, Deserializer, Serializer};
551
552 #[allow(missing_docs)]
553 pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
554 where
555 S: Serializer,
556 {
557 if let Some(fees) = fees {
558 let mut state = serializer.serialize_struct("RoutingFees", 2)?;
559 state.serialize_field("base_msat", &fees.base_msat)?;
560 state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
561 state.end()
562 } else {
563 let state = serializer.serialize_struct("RoutingFees", 0)?;
564 state.end()
565 }
566 }
567
568 #[allow(missing_docs)]
569 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
570 where
571 D: Deserializer<'de>,
572 {
573 let fees = serde_json::Value::deserialize(deserializer)?;
574 let base_msat = fees["base_msat"].as_u64();
576
577 if let Some(base_msat) = base_msat {
578 if let Some(proportional_millionths) = fees["proportional_millionths"].as_u64() {
579 let base_msat: u32 = base_msat
580 .try_into()
581 .map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
582 let proportional_millionths: u32 =
583 proportional_millionths.try_into().map_err(|_| {
584 serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
585 })?;
586 return Ok(Some(RoutingFees {
587 base_msat,
588 proportional_millionths,
589 }));
590 }
591 }
592
593 Ok(None)
594 }
595}
596
597#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
598pub enum LightningInputError {
599 #[error("The input contract {0} does not exist")]
600 UnknownContract(ContractId),
601 #[error("The input contract has too little funds, got {0}, input spends {1}")]
602 InsufficientFunds(Amount, Amount),
603 #[error("An outgoing LN contract spend did not provide a preimage")]
604 MissingPreimage,
605 #[error("An outgoing LN contract spend provided a wrong preimage")]
606 InvalidPreimage,
607 #[error("Incoming contract not ready to be spent yet, decryption in progress")]
608 ContractNotReady,
609 #[error("The lightning input version is not supported by this federation")]
610 UnknownInputVariant(#[from] UnknownLightningInputVariantError),
611}
612
613#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
614pub enum LightningOutputError {
615 #[error("The input contract {0} does not exist")]
616 UnknownContract(ContractId),
617 #[error("Output contract value may not be zero unless it's an offer output")]
618 ZeroOutput,
619 #[error("Offer contains invalid threshold-encrypted data")]
620 InvalidEncryptedPreimage,
621 #[error("Offer contains a ciphertext that has already been used")]
622 DuplicateEncryptedPreimage,
623 #[error("The incoming LN account requires more funding (need {0} got {1})")]
624 InsufficientIncomingFunding(Amount, Amount),
625 #[error("No offer found for payment hash {0}")]
626 NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
627 #[error("Only outgoing contracts support cancellation")]
628 NotOutgoingContract,
629 #[error("Cancellation request wasn't properly signed")]
630 InvalidCancellationSignature,
631 #[error("The lightning output version is not supported by this federation")]
632 UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
633}
634
635pub async fn ln_operation(
636 client: &ClientHandleArc,
637 operation_id: OperationId,
638) -> anyhow::Result<OperationLogEntry> {
639 let operation = client
640 .operation_log()
641 .get_operation(operation_id)
642 .await
643 .ok_or(anyhow::anyhow!("Operation not found"))?;
644
645 if operation.operation_module_kind() != LightningCommonInit::KIND.as_str() {
646 bail!("Operation is not a lightning operation");
647 }
648
649 Ok(operation)
650}
651
652#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
657pub struct PrunedInvoice {
658 pub amount: Amount,
659 pub destination: secp256k1::PublicKey,
660 #[serde(with = "fedimint_core::hex::serde", default)]
662 pub destination_features: Vec<u8>,
663 pub payment_hash: sha256::Hash,
664 pub payment_secret: [u8; 32],
665 pub route_hints: Vec<RouteHint>,
666 pub min_final_cltv_delta: u64,
667 pub expiry_timestamp: u64,
669}
670
671impl PrunedInvoice {
672 pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
673 let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
676
677 let destination_features = if let Some(features) = invoice.features() {
678 encode_bolt11_invoice_features_without_length(features)
679 } else {
680 vec![]
681 };
682
683 PrunedInvoice {
684 amount,
685 destination: invoice
686 .payee_pub_key()
687 .copied()
688 .unwrap_or_else(|| invoice.recover_payee_pub_key()),
689 destination_features,
690 payment_hash: *invoice.payment_hash(),
691 payment_secret: invoice.payment_secret().0,
692 route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
693 min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
694 expiry_timestamp,
695 }
696 }
697}
698
699impl TryFrom<Bolt11Invoice> for PrunedInvoice {
700 type Error = anyhow::Error;
701
702 fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
703 Ok(PrunedInvoice::new(
704 &invoice,
705 Amount::from_msats(
706 invoice
707 .amount_milli_satoshis()
708 .context("Invoice amount is missing")?,
709 ),
710 ))
711 }
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct RemoveGatewayRequest {
720 pub gateway_id: secp256k1::PublicKey,
721 pub signatures: BTreeMap<PeerId, Signature>,
722}
723
724pub fn create_gateway_remove_message(
732 federation_public_key: PublicKey,
733 peer_id: PeerId,
734 challenge: sha256::Hash,
735) -> Message {
736 let mut message_preimage = "remove-gateway".as_bytes().to_vec();
737 message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
738 let guardian_id: u16 = peer_id.into();
739 message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
740 message_preimage.append(&mut challenge.consensus_encode_to_vec());
741 Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
742}