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