#![deny(clippy::pedantic)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]
extern crate core;
pub mod config;
pub mod contracts;
pub mod federation_endpoint_constants;
pub mod gateway_endpoint_constants;
use std::collections::BTreeMap;
use std::io::{Error, ErrorKind, Read, Write};
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as AnyhowContext};
use bitcoin::hashes::{sha256, Hash};
use config::LightningClientConfig;
use fedimint_client::oplog::OperationLogEntry;
use fedimint_client::ClientHandleArc;
use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind, OperationId};
use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
use fedimint_core::module::registry::ModuleDecoderRegistry;
use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
use fedimint_core::secp256k1::Message;
use fedimint_core::util::SafeUrl;
use fedimint_core::{
encode_bolt11_invoice_features_without_length, extensible_associated_module_type,
plugin_types_trait_impl_common, secp256k1, Amount, PeerId,
};
use lightning_invoice::{Bolt11Invoice, RoutingFees};
use secp256k1::schnorr::Signature;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use threshold_crypto::PublicKey;
use tracing::error;
pub use {bitcoin, lightning_invoice};
use crate::contracts::incoming::OfferId;
use crate::contracts::{Contract, ContractId, ContractOutcome, Preimage, PreimageDecryptionShare};
use crate::route_hints::RouteHint;
pub const KIND: ModuleKind = ModuleKind::from_static_str("ln");
pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 0);
extensible_associated_module_type!(
LightningInput,
LightningInputV0,
UnknownLightningInputVariantError
);
impl LightningInput {
pub fn new_v0(
contract_id: ContractId,
amount: Amount,
witness: Option<Preimage>,
) -> LightningInput {
LightningInput::V0(LightningInputV0 {
contract_id,
amount,
witness,
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct LightningInputV0 {
pub contract_id: contracts::ContractId,
pub amount: Amount,
pub witness: Option<Preimage>,
}
impl std::fmt::Display for LightningInputV0 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Lightning Contract {} with amount {}",
self.contract_id, self.amount
)
}
}
extensible_associated_module_type!(
LightningOutput,
LightningOutputV0,
UnknownLightningOutputVariantError
);
impl LightningOutput {
pub fn new_v0_contract(contract: ContractOutput) -> LightningOutput {
LightningOutput::V0(LightningOutputV0::Contract(contract))
}
pub fn new_v0_offer(offer: contracts::incoming::IncomingContractOffer) -> LightningOutput {
LightningOutput::V0(LightningOutputV0::Offer(offer))
}
pub fn new_v0_cancel_outgoing(
contract: ContractId,
gateway_signature: secp256k1::schnorr::Signature,
) -> LightningOutput {
LightningOutput::V0(LightningOutputV0::CancelOutgoing {
contract,
gateway_signature,
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub enum LightningOutputV0 {
Contract(ContractOutput),
Offer(contracts::incoming::IncomingContractOffer),
CancelOutgoing {
contract: ContractId,
gateway_signature: fedimint_core::secp256k1::schnorr::Signature,
},
}
impl std::fmt::Display for LightningOutputV0 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LightningOutputV0::Contract(ContractOutput { amount, contract }) => match contract {
Contract::Incoming(incoming) => {
write!(
f,
"LN Incoming Contract for {} hash {}",
amount, incoming.hash
)
}
Contract::Outgoing(outgoing) => {
write!(
f,
"LN Outgoing Contract for {} hash {}",
amount, outgoing.hash
)
}
},
LightningOutputV0::Offer(offer) => {
write!(f, "LN offer for {} with hash {}", offer.amount, offer.hash)
}
LightningOutputV0::CancelOutgoing { contract, .. } => {
write!(f, "LN outgoing contract cancellation {contract}")
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct ContractOutput {
pub amount: fedimint_core::Amount,
pub contract: contracts::Contract,
}
#[derive(Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize, Clone)]
pub struct ContractAccount {
pub amount: fedimint_core::Amount,
pub contract: contracts::FundedContract,
}
extensible_associated_module_type!(
LightningOutputOutcome,
LightningOutputOutcomeV0,
UnknownLightningOutputOutcomeVariantError
);
impl LightningOutputOutcome {
pub fn new_v0_contract(id: ContractId, outcome: ContractOutcome) -> LightningOutputOutcome {
LightningOutputOutcome::V0(LightningOutputOutcomeV0::Contract { id, outcome })
}
pub fn new_v0_offer(id: OfferId) -> LightningOutputOutcome {
LightningOutputOutcome::V0(LightningOutputOutcomeV0::Offer { id })
}
pub fn new_v0_cancel_outgoing(id: ContractId) -> LightningOutputOutcome {
LightningOutputOutcome::V0(LightningOutputOutcomeV0::CancelOutgoingContract { id })
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub enum LightningOutputOutcomeV0 {
Contract {
id: ContractId,
outcome: ContractOutcome,
},
Offer {
id: OfferId,
},
CancelOutgoingContract {
id: ContractId,
},
}
impl LightningOutputOutcomeV0 {
pub fn is_permanent(&self) -> bool {
match self {
LightningOutputOutcomeV0::Contract { id: _, outcome } => outcome.is_permanent(),
LightningOutputOutcomeV0::Offer { .. }
| LightningOutputOutcomeV0::CancelOutgoingContract { .. } => true,
}
}
}
impl std::fmt::Display for LightningOutputOutcomeV0 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LightningOutputOutcomeV0::Contract { id, .. } => {
write!(f, "LN Contract {id}")
}
LightningOutputOutcomeV0::Offer { id } => {
write!(f, "LN Offer {id}")
}
LightningOutputOutcomeV0::CancelOutgoingContract { id: contract_id } => {
write!(f, "LN Outgoing Contract Cancellation {contract_id}")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LightningGatewayRegistration {
pub info: LightningGateway,
pub vetted: bool,
pub valid_until: SystemTime,
}
impl Encodable for LightningGatewayRegistration {
fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
let json_repr = serde_json::to_string(self).map_err(|e| {
Error::new(
ErrorKind::Other,
format!("Failed to serialize LightningGatewayRegistration: {e}"),
)
})?;
json_repr.consensus_encode(writer)
}
}
impl Decodable for LightningGatewayRegistration {
fn consensus_decode<R: Read>(
r: &mut R,
modules: &ModuleDecoderRegistry,
) -> Result<Self, DecodeError> {
let json_repr = String::consensus_decode(r, modules)?;
serde_json::from_str(&json_repr).map_err(|e| {
DecodeError::new_custom(
anyhow::Error::new(e).context("Failed to deserialize LightningGatewayRegistration"),
)
})
}
}
impl LightningGatewayRegistration {
pub fn unanchor(self) -> LightningGatewayAnnouncement {
LightningGatewayAnnouncement {
info: self.info,
ttl: self
.valid_until
.duration_since(fedimint_core::time::now())
.unwrap_or_default(),
vetted: self.vetted,
}
}
pub fn is_expired(&self) -> bool {
self.valid_until < fedimint_core::time::now()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LightningGatewayAnnouncement {
pub info: LightningGateway,
pub vetted: bool,
pub ttl: Duration,
}
impl LightningGatewayAnnouncement {
pub fn anchor(self) -> LightningGatewayRegistration {
LightningGatewayRegistration {
info: self.info,
vetted: self.vetted,
valid_until: fedimint_core::time::now() + self.ttl,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq, Hash)]
pub struct LightningGateway {
#[serde(rename = "mint_channel_id")]
pub federation_index: u64,
pub gateway_redeem_key: fedimint_core::secp256k1::PublicKey,
pub node_pub_key: fedimint_core::secp256k1::PublicKey,
pub lightning_alias: String,
pub api: SafeUrl,
pub route_hints: Vec<route_hints::RouteHint>,
#[serde(with = "serde_routing_fees")]
pub fees: RoutingFees,
pub gateway_id: secp256k1::PublicKey,
pub supports_private_payments: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Encodable, Decodable, Serialize, Deserialize)]
pub enum LightningConsensusItem {
DecryptPreimage(ContractId, PreimageDecryptionShare),
BlockCount(u64),
#[encodable_default]
Default {
variant: u64,
bytes: Vec<u8>,
},
}
impl std::fmt::Display for LightningConsensusItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LightningConsensusItem::DecryptPreimage(contract_id, _) => {
write!(f, "LN Decryption Share for contract {contract_id}")
}
LightningConsensusItem::BlockCount(count) => write!(f, "LN block count {count}"),
LightningConsensusItem::Default { variant, .. } => {
write!(f, "Unknown LN CI variant={variant}")
}
}
}
}
#[derive(Debug)]
pub struct LightningCommonInit;
impl CommonModuleInit for LightningCommonInit {
const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
const KIND: ModuleKind = KIND;
type ClientConfig = LightningClientConfig;
fn decoder() -> Decoder {
LightningModuleTypes::decoder()
}
}
pub struct LightningModuleTypes;
plugin_types_trait_impl_common!(
KIND,
LightningModuleTypes,
LightningClientConfig,
LightningInput,
LightningOutput,
LightningOutputOutcome,
LightningConsensusItem,
LightningInputError,
LightningOutputError
);
pub mod route_hints {
use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_core::secp256k1::PublicKey;
use lightning_invoice::RoutingFees;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
pub struct RouteHintHop {
pub src_node_id: PublicKey,
pub short_channel_id: u64,
pub base_msat: u32,
pub proportional_millionths: u32,
pub cltv_expiry_delta: u16,
pub htlc_minimum_msat: Option<u64>,
pub htlc_maximum_msat: Option<u64>,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
pub struct RouteHint(pub Vec<RouteHintHop>);
impl RouteHint {
pub fn to_ldk_route_hint(&self) -> lightning_invoice::RouteHint {
lightning_invoice::RouteHint(
self.0
.iter()
.map(|hop| lightning_invoice::RouteHintHop {
src_node_id: hop.src_node_id,
short_channel_id: hop.short_channel_id,
fees: RoutingFees {
base_msat: hop.base_msat,
proportional_millionths: hop.proportional_millionths,
},
cltv_expiry_delta: hop.cltv_expiry_delta,
htlc_minimum_msat: hop.htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
})
.collect(),
)
}
}
impl From<lightning_invoice::RouteHint> for RouteHint {
fn from(rh: lightning_invoice::RouteHint) -> Self {
RouteHint(rh.0.into_iter().map(Into::into).collect())
}
}
impl From<lightning_invoice::RouteHintHop> for RouteHintHop {
fn from(rhh: lightning_invoice::RouteHintHop) -> Self {
RouteHintHop {
src_node_id: rhh.src_node_id,
short_channel_id: rhh.short_channel_id,
base_msat: rhh.fees.base_msat,
proportional_millionths: rhh.fees.proportional_millionths,
cltv_expiry_delta: rhh.cltv_expiry_delta,
htlc_minimum_msat: rhh.htlc_minimum_msat,
htlc_maximum_msat: rhh.htlc_maximum_msat,
}
}
}
}
pub mod serde_routing_fees {
use lightning_invoice::RoutingFees;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serializer};
#[allow(missing_docs)]
pub fn serialize<S>(fees: &RoutingFees, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("RoutingFees", 2)?;
state.serialize_field("base_msat", &fees.base_msat)?;
state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
state.end()
}
#[allow(missing_docs)]
pub fn deserialize<'de, D>(deserializer: D) -> Result<RoutingFees, D::Error>
where
D: Deserializer<'de>,
{
let fees = serde_json::Value::deserialize(deserializer)?;
let base_msat = fees["base_msat"]
.as_u64()
.ok_or_else(|| serde::de::Error::custom("base_msat is not a u64"))?;
let proportional_millionths = fees["proportional_millionths"]
.as_u64()
.ok_or_else(|| serde::de::Error::custom("proportional_millionths is not a u64"))?;
Ok(RoutingFees {
base_msat: base_msat
.try_into()
.map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?,
proportional_millionths: proportional_millionths.try_into().map_err(|_| {
serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
})?,
})
}
}
pub mod serde_option_routing_fees {
use lightning_invoice::RoutingFees;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serializer};
#[allow(missing_docs)]
pub fn serialize<S>(fees: &Option<RoutingFees>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(fees) = fees {
let mut state = serializer.serialize_struct("RoutingFees", 2)?;
state.serialize_field("base_msat", &fees.base_msat)?;
state.serialize_field("proportional_millionths", &fees.proportional_millionths)?;
state.end()
} else {
let state = serializer.serialize_struct("RoutingFees", 0)?;
state.end()
}
}
#[allow(missing_docs)]
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RoutingFees>, D::Error>
where
D: Deserializer<'de>,
{
let fees = serde_json::Value::deserialize(deserializer)?;
let base_msat = fees["base_msat"].as_u64();
if let Some(base_msat) = base_msat {
if let Some(proportional_millionths) = fees["proportional_millionths"].as_u64() {
let base_msat: u32 = base_msat
.try_into()
.map_err(|_| serde::de::Error::custom("base_msat is greater than u32::MAX"))?;
let proportional_millionths: u32 =
proportional_millionths.try_into().map_err(|_| {
serde::de::Error::custom("proportional_millionths is greater than u32::MAX")
})?;
return Ok(Some(RoutingFees {
base_msat,
proportional_millionths,
}));
}
}
Ok(None)
}
}
#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
pub enum LightningInputError {
#[error("The input contract {0} does not exist")]
UnknownContract(ContractId),
#[error("The input contract has too little funds, got {0}, input spends {1}")]
InsufficientFunds(Amount, Amount),
#[error("An outgoing LN contract spend did not provide a preimage")]
MissingPreimage,
#[error("An outgoing LN contract spend provided a wrong preimage")]
InvalidPreimage,
#[error("Incoming contract not ready to be spent yet, decryption in progress")]
ContractNotReady,
#[error("The lightning input version is not supported by this federation")]
UnknownInputVariant(#[from] UnknownLightningInputVariantError),
}
#[derive(Debug, Error, Eq, PartialEq, Encodable, Decodable, Hash, Clone)]
pub enum LightningOutputError {
#[error("The input contract {0} does not exist")]
UnknownContract(ContractId),
#[error("Output contract value may not be zero unless it's an offer output")]
ZeroOutput,
#[error("Offer contains invalid threshold-encrypted data")]
InvalidEncryptedPreimage,
#[error("Offer contains a ciphertext that has already been used")]
DuplicateEncryptedPreimage,
#[error("The incoming LN account requires more funding (need {0} got {1})")]
InsufficientIncomingFunding(Amount, Amount),
#[error("No offer found for payment hash {0}")]
NoOffer(bitcoin::secp256k1::hashes::sha256::Hash),
#[error("Only outgoing contracts support cancellation")]
NotOutgoingContract,
#[error("Cancellation request wasn't properly signed")]
InvalidCancellationSignature,
#[error("The lightning output version is not supported by this federation")]
UnknownOutputVariant(#[from] UnknownLightningOutputVariantError),
}
pub async fn ln_operation(
client: &ClientHandleArc,
operation_id: OperationId,
) -> anyhow::Result<OperationLogEntry> {
let operation = client
.operation_log()
.get_operation(operation_id)
.await
.ok_or(anyhow::anyhow!("Operation not found"))?;
if operation.operation_module_kind() != LightningCommonInit::KIND.as_str() {
bail!("Operation is not a lightning operation");
}
Ok(operation)
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
pub struct PrunedInvoice {
pub amount: Amount,
pub destination: secp256k1::PublicKey,
#[serde(with = "fedimint_core::hex::serde", default)]
pub destination_features: Vec<u8>,
pub payment_hash: sha256::Hash,
pub payment_secret: [u8; 32],
pub route_hints: Vec<RouteHint>,
pub min_final_cltv_delta: u64,
pub expiry_timestamp: u64,
}
impl PrunedInvoice {
pub fn new(invoice: &Bolt11Invoice, amount: Amount) -> Self {
let expiry_timestamp = invoice.expires_at().map_or(u64::MAX, |t| t.as_secs());
let destination_features = if let Some(features) = invoice.features() {
encode_bolt11_invoice_features_without_length(features)
} else {
vec![]
};
PrunedInvoice {
amount,
destination: invoice
.payee_pub_key()
.copied()
.unwrap_or_else(|| invoice.recover_payee_pub_key()),
destination_features,
payment_hash: *invoice.payment_hash(),
payment_secret: invoice.payment_secret().0,
route_hints: invoice.route_hints().into_iter().map(Into::into).collect(),
min_final_cltv_delta: invoice.min_final_cltv_expiry_delta(),
expiry_timestamp,
}
}
}
impl TryFrom<Bolt11Invoice> for PrunedInvoice {
type Error = anyhow::Error;
fn try_from(invoice: Bolt11Invoice) -> Result<Self, Self::Error> {
Ok(PrunedInvoice::new(
&invoice,
Amount::from_msats(
invoice
.amount_milli_satoshis()
.context("Invoice amount is missing")?,
),
))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveGatewayRequest {
pub gateway_id: secp256k1::PublicKey,
pub signatures: BTreeMap<PeerId, Signature>,
}
pub fn create_gateway_remove_message(
federation_public_key: PublicKey,
peer_id: PeerId,
challenge: sha256::Hash,
) -> Message {
let mut message_preimage = "remove-gateway".as_bytes().to_vec();
message_preimage.append(&mut federation_public_key.consensus_encode_to_vec());
let guardian_id: u16 = peer_id.into();
message_preimage.append(&mut guardian_id.consensus_encode_to_vec());
message_preimage.append(&mut challenge.consensus_encode_to_vec());
Message::from_digest(*sha256::Hash::hash(message_preimage.as_slice()).as_ref())
}