fedimint_gwv2_client/
lib.rs

1mod api;
2mod complete_sm;
3pub mod events;
4mod receive_sm;
5mod send_sm;
6
7use std::collections::BTreeMap;
8use std::fmt;
9use std::fmt::Debug;
10use std::sync::Arc;
11
12use anyhow::{anyhow, ensure};
13use async_trait::async_trait;
14use bitcoin::hashes::sha256;
15use bitcoin::secp256k1::Message;
16use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
17use fedimint_api_client::api::DynModuleApi;
18use fedimint_client::ClientHandleArc;
19use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
20use fedimint_client_module::module::recovery::NoModuleBackup;
21use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
22use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
23use fedimint_client_module::transaction::{
24    ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
25};
26use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
27use fedimint_core::config::FederationId;
28use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
29use fedimint_core::db::DatabaseTransaction;
30use fedimint_core::encoding::{Decodable, Encodable};
31use fedimint_core::module::{
32    Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
33};
34use fedimint_core::secp256k1::Keypair;
35use fedimint_core::time::now;
36use fedimint_core::util::Spanned;
37use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send, secp256k1};
38use fedimint_lightning::{InterceptPaymentResponse, LightningRpcError};
39use fedimint_lnv2_common::config::LightningClientConfig;
40use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
41use fedimint_lnv2_common::gateway_api::SendPaymentPayload;
42use fedimint_lnv2_common::{
43    LightningCommonInit, LightningInvoice, LightningModuleTypes, LightningOutput, LightningOutputV0,
44};
45use futures::StreamExt;
46use lightning_invoice::Bolt11Invoice;
47use receive_sm::{ReceiveSMState, ReceiveStateMachine};
48use secp256k1::schnorr::Signature;
49use send_sm::{SendSMState, SendStateMachine};
50use serde::{Deserialize, Serialize};
51use tpe::{AggregatePublicKey, PublicKeyShare};
52use tracing::{info, warn};
53
54use crate::api::GatewayFederationApi;
55use crate::complete_sm::{CompleteSMCommon, CompleteSMState, CompleteStateMachine};
56use crate::receive_sm::ReceiveSMCommon;
57use crate::send_sm::SendSMCommon;
58
59/// LNv2 CLTV Delta in blocks
60pub const EXPIRATION_DELTA_MINIMUM_V2: u64 = 144;
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct GatewayOperationMetaV2;
64
65#[derive(Debug, Clone)]
66pub struct GatewayClientInitV2 {
67    pub gateway: Arc<dyn IGatewayClientV2>,
68}
69
70impl ModuleInit for GatewayClientInitV2 {
71    type Common = LightningCommonInit;
72
73    async fn dump_database(
74        &self,
75        _dbtx: &mut DatabaseTransaction<'_>,
76        _prefix_names: Vec<String>,
77    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
78        Box::new(vec![].into_iter())
79    }
80}
81
82#[apply(async_trait_maybe_send!)]
83impl ClientModuleInit for GatewayClientInitV2 {
84    type Module = GatewayClientModuleV2;
85
86    fn supported_api_versions(&self) -> MultiApiVersion {
87        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
88            .expect("no version conflicts")
89    }
90
91    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
92        Ok(GatewayClientModuleV2 {
93            federation_id: *args.federation_id(),
94            cfg: args.cfg().clone(),
95            notifier: args.notifier().clone(),
96            client_ctx: args.context(),
97            module_api: args.module_api().clone(),
98            keypair: args
99                .module_root_secret()
100                .clone()
101                .to_secp_key(fedimint_core::secp256k1::SECP256K1),
102            gateway: self.gateway.clone(),
103        })
104    }
105}
106
107#[derive(Debug, Clone)]
108pub struct GatewayClientModuleV2 {
109    pub federation_id: FederationId,
110    pub cfg: LightningClientConfig,
111    pub notifier: ModuleNotifier<GatewayClientStateMachinesV2>,
112    pub client_ctx: ClientContext<Self>,
113    pub module_api: DynModuleApi,
114    pub keypair: Keypair,
115    pub gateway: Arc<dyn IGatewayClientV2>,
116}
117
118#[derive(Debug, Clone)]
119pub struct GatewayClientContextV2 {
120    pub module: GatewayClientModuleV2,
121    pub decoder: Decoder,
122    pub tpe_agg_pk: AggregatePublicKey,
123    pub tpe_pks: BTreeMap<PeerId, PublicKeyShare>,
124    pub gateway: Arc<dyn IGatewayClientV2>,
125}
126
127impl Context for GatewayClientContextV2 {
128    const KIND: Option<ModuleKind> = Some(fedimint_lnv2_common::KIND);
129}
130
131impl ClientModule for GatewayClientModuleV2 {
132    type Init = GatewayClientInitV2;
133    type Common = LightningModuleTypes;
134    type Backup = NoModuleBackup;
135    type ModuleStateMachineContext = GatewayClientContextV2;
136    type States = GatewayClientStateMachinesV2;
137
138    fn context(&self) -> Self::ModuleStateMachineContext {
139        GatewayClientContextV2 {
140            module: self.clone(),
141            decoder: self.decoder(),
142            tpe_agg_pk: self.cfg.tpe_agg_pk,
143            tpe_pks: self.cfg.tpe_pks.clone(),
144            gateway: self.gateway.clone(),
145        }
146    }
147    fn input_fee(
148        &self,
149        amount: &Amounts,
150        _input: &<Self::Common as ModuleCommon>::Input,
151    ) -> Option<Amounts> {
152        Some(Amounts::new_bitcoin(
153            self.cfg.fee_consensus.fee(amount.expect_only_bitcoin()),
154        ))
155    }
156
157    fn output_fee(
158        &self,
159        _amount: &Amounts,
160        output: &<Self::Common as ModuleCommon>::Output,
161    ) -> Option<Amounts> {
162        let amount = match output.ensure_v0_ref().ok()? {
163            LightningOutputV0::Outgoing(contract) => contract.amount,
164            LightningOutputV0::Incoming(contract) => contract.commitment.amount,
165        };
166
167        Some(Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)))
168    }
169}
170
171#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
172pub enum GatewayClientStateMachinesV2 {
173    Send(SendStateMachine),
174    Receive(ReceiveStateMachine),
175    Complete(CompleteStateMachine),
176}
177
178impl fmt::Display for GatewayClientStateMachinesV2 {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            GatewayClientStateMachinesV2::Send(send) => {
182                write!(f, "{send}")
183            }
184            GatewayClientStateMachinesV2::Receive(receive) => {
185                write!(f, "{receive}")
186            }
187            GatewayClientStateMachinesV2::Complete(complete) => {
188                write!(f, "{complete}")
189            }
190        }
191    }
192}
193
194impl IntoDynInstance for GatewayClientStateMachinesV2 {
195    type DynType = DynState;
196
197    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
198        DynState::from_typed(instance_id, self)
199    }
200}
201
202impl State for GatewayClientStateMachinesV2 {
203    type ModuleContext = GatewayClientContextV2;
204
205    fn transitions(
206        &self,
207        context: &Self::ModuleContext,
208        global_context: &DynGlobalClientContext,
209    ) -> Vec<StateTransition<Self>> {
210        match self {
211            GatewayClientStateMachinesV2::Send(state) => {
212                sm_enum_variant_translation!(
213                    state.transitions(context, global_context),
214                    GatewayClientStateMachinesV2::Send
215                )
216            }
217            GatewayClientStateMachinesV2::Receive(state) => {
218                sm_enum_variant_translation!(
219                    state.transitions(context, global_context),
220                    GatewayClientStateMachinesV2::Receive
221                )
222            }
223            GatewayClientStateMachinesV2::Complete(state) => {
224                sm_enum_variant_translation!(
225                    state.transitions(context, global_context),
226                    GatewayClientStateMachinesV2::Complete
227                )
228            }
229        }
230    }
231
232    fn operation_id(&self) -> OperationId {
233        match self {
234            GatewayClientStateMachinesV2::Send(state) => state.operation_id(),
235            GatewayClientStateMachinesV2::Receive(state) => state.operation_id(),
236            GatewayClientStateMachinesV2::Complete(state) => state.operation_id(),
237        }
238    }
239}
240
241#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
242pub enum FinalReceiveState {
243    Rejected,
244    Success([u8; 32]),
245    Refunded,
246    Failure,
247}
248
249impl GatewayClientModuleV2 {
250    pub async fn send_payment(
251        &self,
252        payload: SendPaymentPayload,
253    ) -> anyhow::Result<Result<[u8; 32], Signature>> {
254        let operation_start = now();
255
256        // The operation id is equal to the contract id which also doubles as the
257        // message signed by the gateway via the forfeit signature to forfeit
258        // the gateways claim to a contract in case of cancellation. We only create a
259        // forfeit signature after we have started the send state machine to
260        // prevent replay attacks with a previously cancelled outgoing contract
261        let operation_id = OperationId::from_encodable(&payload.contract.clone());
262
263        if self.client_ctx.operation_exists(operation_id).await {
264            return Ok(self.subscribe_send(operation_id).await);
265        }
266
267        // Since the following four checks may only fail due to client side
268        // programming error we do not have to enable cancellation and can check
269        // them before we start the state machine.
270        ensure!(
271            payload.contract.claim_pk == self.keypair.public_key(),
272            "The outgoing contract is keyed to another gateway"
273        );
274
275        // This prevents DOS attacks where an attacker submits a different invoice.
276        ensure!(
277            secp256k1::SECP256K1
278                .verify_schnorr(
279                    &payload.auth,
280                    &Message::from_digest(
281                        *payload.invoice.consensus_hash::<sha256::Hash>().as_ref()
282                    ),
283                    &payload.contract.refund_pk.x_only_public_key().0,
284                )
285                .is_ok(),
286            "Invalid auth signature for the invoice data"
287        );
288
289        // We need to check that the contract has been confirmed by the federation
290        // before we start the state machine to prevent DOS attacks.
291        let (contract_id, expiration) = self
292            .module_api
293            .outgoing_contract_expiration(payload.outpoint)
294            .await
295            .map_err(|_| anyhow!("The gateway can not reach the federation"))?
296            .ok_or(anyhow!("The outgoing contract has not yet been confirmed"))?;
297
298        ensure!(
299            contract_id == payload.contract.contract_id(),
300            "Contract Id returned by the federation does not match contract in request"
301        );
302
303        let (payment_hash, amount) = match &payload.invoice {
304            LightningInvoice::Bolt11(invoice) => (
305                invoice.payment_hash(),
306                invoice
307                    .amount_milli_satoshis()
308                    .ok_or(anyhow!("Invoice is missing amount"))?,
309            ),
310        };
311
312        ensure!(
313            PaymentImage::Hash(*payment_hash) == payload.contract.payment_image,
314            "The invoices payment hash does not match the contracts payment hash"
315        );
316
317        let min_contract_amount = self
318            .gateway
319            .min_contract_amount(&payload.federation_id, amount)
320            .await?;
321
322        let send_sm = GatewayClientStateMachinesV2::Send(SendStateMachine {
323            common: SendSMCommon {
324                operation_id,
325                outpoint: payload.outpoint,
326                contract: payload.contract.clone(),
327                max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
328                min_contract_amount,
329                invoice: payload.invoice,
330                claim_keypair: self.keypair,
331            },
332            state: SendSMState::Sending,
333        });
334
335        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
336        self.client_ctx
337            .manual_operation_start_dbtx(
338                &mut dbtx.to_ref_nc(),
339                operation_id,
340                LightningCommonInit::KIND.as_str(),
341                GatewayOperationMetaV2,
342                vec![self.client_ctx.make_dyn_state(send_sm)],
343            )
344            .await
345            .ok();
346
347        self.client_ctx
348            .log_event(
349                &mut dbtx,
350                OutgoingPaymentStarted {
351                    operation_start,
352                    outgoing_contract: payload.contract.clone(),
353                    min_contract_amount,
354                    invoice_amount: Amount::from_msats(amount),
355                    max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
356                },
357            )
358            .await;
359        dbtx.commit_tx().await;
360
361        Ok(self.subscribe_send(operation_id).await)
362    }
363
364    pub async fn subscribe_send(&self, operation_id: OperationId) -> Result<[u8; 32], Signature> {
365        let mut stream = self.notifier.subscribe(operation_id).await;
366
367        loop {
368            if let Some(GatewayClientStateMachinesV2::Send(state)) = stream.next().await {
369                match state.state {
370                    SendSMState::Sending => {}
371                    SendSMState::Claiming(claiming) => {
372                        // This increases latency by one ordering and may eventually be removed;
373                        // however, at the current stage of lnv2 we prioritize the verification of
374                        // correctness above minimum latency.
375                        assert!(
376                            self.client_ctx
377                                .await_primary_module_outputs(operation_id, claiming.outpoints)
378                                .await
379                                .is_ok(),
380                            "Gateway Module V2 failed to claim outgoing contract with preimage"
381                        );
382
383                        return Ok(claiming.preimage);
384                    }
385                    SendSMState::Cancelled(cancelled) => {
386                        warn!("Outgoing lightning payment is cancelled {:?}", cancelled);
387
388                        let signature = self
389                            .keypair
390                            .sign_schnorr(state.common.contract.forfeit_message());
391
392                        assert!(state.common.contract.verify_forfeit_signature(&signature));
393
394                        return Err(signature);
395                    }
396                }
397            }
398        }
399    }
400
401    pub async fn relay_incoming_htlc(
402        &self,
403        payment_hash: sha256::Hash,
404        incoming_chan_id: u64,
405        htlc_id: u64,
406        contract: IncomingContract,
407        amount_msat: u64,
408    ) -> anyhow::Result<()> {
409        let operation_start = now();
410
411        let operation_id = OperationId::from_encodable(&contract);
412
413        if self.client_ctx.operation_exists(operation_id).await {
414            return Ok(());
415        }
416
417        let refund_keypair = self.keypair;
418
419        let client_output = ClientOutput::<LightningOutput> {
420            output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
421            amounts: Amounts::new_bitcoin(contract.commitment.amount),
422        };
423        let commitment = contract.commitment.clone();
424        let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
425            state_machines: Arc::new(move |range: OutPointRange| {
426                assert_eq!(range.count(), 1);
427
428                vec![
429                    GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
430                        common: ReceiveSMCommon {
431                            operation_id,
432                            contract: contract.clone(),
433                            outpoint: range.into_iter().next().unwrap(),
434                            refund_keypair,
435                        },
436                        state: ReceiveSMState::Funding,
437                    }),
438                    GatewayClientStateMachinesV2::Complete(CompleteStateMachine {
439                        common: CompleteSMCommon {
440                            operation_id,
441                            payment_hash,
442                            incoming_chan_id,
443                            htlc_id,
444                        },
445                        state: CompleteSMState::Pending,
446                    }),
447                ]
448            }),
449        };
450
451        let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
452            vec![client_output],
453            vec![client_output_sm],
454        ));
455        let transaction = TransactionBuilder::new().with_outputs(client_output);
456
457        self.client_ctx
458            .finalize_and_submit_transaction(
459                operation_id,
460                LightningCommonInit::KIND.as_str(),
461                |_| GatewayOperationMetaV2,
462                transaction,
463            )
464            .await?;
465
466        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
467        self.client_ctx
468            .log_event(
469                &mut dbtx,
470                IncomingPaymentStarted {
471                    operation_start,
472                    incoming_contract_commitment: commitment,
473                    invoice_amount: Amount::from_msats(amount_msat),
474                },
475            )
476            .await;
477        dbtx.commit_tx().await;
478
479        Ok(())
480    }
481
482    pub async fn relay_direct_swap(
483        &self,
484        contract: IncomingContract,
485        amount_msat: u64,
486    ) -> anyhow::Result<FinalReceiveState> {
487        let operation_start = now();
488
489        let operation_id = OperationId::from_encodable(&contract);
490
491        if self.client_ctx.operation_exists(operation_id).await {
492            return Ok(self.await_receive(operation_id).await);
493        }
494
495        let refund_keypair = self.keypair;
496
497        let client_output = ClientOutput::<LightningOutput> {
498            output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
499            amounts: Amounts::new_bitcoin(contract.commitment.amount),
500        };
501        let commitment = contract.commitment.clone();
502        let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
503            state_machines: Arc::new(move |range| {
504                assert_eq!(range.count(), 1);
505
506                vec![GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
507                    common: ReceiveSMCommon {
508                        operation_id,
509                        contract: contract.clone(),
510                        outpoint: range.into_iter().next().unwrap(),
511                        refund_keypair,
512                    },
513                    state: ReceiveSMState::Funding,
514                })]
515            }),
516        };
517
518        let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
519            vec![client_output],
520            vec![client_output_sm],
521        ));
522
523        let transaction = TransactionBuilder::new().with_outputs(client_output);
524
525        self.client_ctx
526            .finalize_and_submit_transaction(
527                operation_id,
528                LightningCommonInit::KIND.as_str(),
529                |_| GatewayOperationMetaV2,
530                transaction,
531            )
532            .await?;
533
534        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
535        self.client_ctx
536            .log_event(
537                &mut dbtx,
538                IncomingPaymentStarted {
539                    operation_start,
540                    incoming_contract_commitment: commitment,
541                    invoice_amount: Amount::from_msats(amount_msat),
542                },
543            )
544            .await;
545        dbtx.commit_tx().await;
546
547        Ok(self.await_receive(operation_id).await)
548    }
549
550    pub async fn await_receive(&self, operation_id: OperationId) -> FinalReceiveState {
551        let mut stream = self.notifier.subscribe(operation_id).await;
552
553        loop {
554            if let Some(GatewayClientStateMachinesV2::Receive(state)) = stream.next().await {
555                match state.state {
556                    ReceiveSMState::Funding => {}
557                    ReceiveSMState::Rejected(..) => return FinalReceiveState::Rejected,
558                    ReceiveSMState::Success(preimage) => {
559                        return FinalReceiveState::Success(preimage);
560                    }
561                    ReceiveSMState::Refunding(out_points) => {
562                        if self
563                            .client_ctx
564                            .await_primary_module_outputs(operation_id, out_points)
565                            .await
566                            .is_err()
567                        {
568                            return FinalReceiveState::Failure;
569                        }
570
571                        return FinalReceiveState::Refunded;
572                    }
573                    ReceiveSMState::Failure => return FinalReceiveState::Failure,
574                }
575            }
576        }
577    }
578
579    /// For the given `OperationId`, this function will wait until the Complete
580    /// state machine has finished or failed.
581    pub async fn await_completion(&self, operation_id: OperationId) {
582        let mut stream = self.notifier.subscribe(operation_id).await;
583
584        loop {
585            match stream.next().await {
586                Some(GatewayClientStateMachinesV2::Complete(state)) => {
587                    if state.state == CompleteSMState::Completed {
588                        info!(%state, "LNv2 completion state machine finished");
589                        return;
590                    }
591
592                    info!(%state, "Waiting for LNv2 completion state machine");
593                }
594                Some(GatewayClientStateMachinesV2::Receive(state)) => {
595                    info!(%state, "Waiting for LNv2 completion state machine");
596                    continue;
597                }
598                Some(state) => {
599                    warn!(%state, "Operation is not an LNv2 completion state machine");
600                    return;
601                }
602                None => return,
603            }
604        }
605    }
606}
607
608/// An interface between module implementation and the general `Gateway`
609///
610/// To abstract away and decouple the core gateway from the modules, the
611/// interface between the is expressed as a trait. The core gateway handles
612/// LNv2 operations that require access to the database or lightning node.
613#[async_trait]
614pub trait IGatewayClientV2: Debug + Send + Sync {
615    /// Use the gateway's lightning node to complete a payment
616    async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse);
617
618    /// Determines if the payment can be completed using a direct swap to
619    /// another federation.
620    ///
621    /// A direct swap is determined by checking the gateway's connected
622    /// lightning node against the invoice's payee lightning node. If they
623    /// are the same, then the gateway can use another client to complete
624    /// the payment be swapping ecash instead of a payment over the
625    /// Lightning network.
626    async fn is_direct_swap(
627        &self,
628        invoice: &Bolt11Invoice,
629    ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>>;
630
631    /// Initiates a payment over the Lightning network.
632    async fn pay(
633        &self,
634        invoice: Bolt11Invoice,
635        max_delay: u64,
636        max_fee: Amount,
637    ) -> Result<[u8; 32], LightningRpcError>;
638
639    /// Computes the minimum contract amount necessary for making an outgoing
640    /// payment.
641    ///
642    /// The minimum contract amount must contain transaction fees to cover the
643    /// gateway's transaction fee and optionally additional fee to cover the
644    /// gateway's Lightning fee if the payment goes over the Lightning
645    /// network.
646    async fn min_contract_amount(
647        &self,
648        federation_id: &FederationId,
649        amount: u64,
650    ) -> anyhow::Result<Amount>;
651
652    /// Check if this invoice was created using LNv1 and if the gateway is
653    /// connected to the target federation.
654    async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>>;
655
656    /// Perform a swap from an LNv2 `OutgoingContract` to an LNv1
657    /// `IncomingContract`
658    async fn relay_lnv1_swap(
659        &self,
660        client: &ClientHandleArc,
661        invoice: &Bolt11Invoice,
662    ) -> anyhow::Result<FinalReceiveState>;
663}