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
59pub 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 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 ensure!(
271 payload.contract.claim_pk == self.keypair.public_key(),
272 "The outgoing contract is keyed to another gateway"
273 );
274
275 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 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 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 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#[async_trait]
614pub trait IGatewayClientV2: Debug + Send + Sync {
615 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse);
617
618 async fn is_direct_swap(
627 &self,
628 invoice: &Bolt11Invoice,
629 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>>;
630
631 async fn pay(
633 &self,
634 invoice: Bolt11Invoice,
635 max_delay: u64,
636 max_fee: Amount,
637 ) -> Result<[u8; 32], LightningRpcError>;
638
639 async fn min_contract_amount(
647 &self,
648 federation_id: &FederationId,
649 amount: u64,
650 ) -> anyhow::Result<Amount>;
651
652 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>>;
655
656 async fn relay_lnv1_swap(
659 &self,
660 client: &ClientHandleArc,
661 invoice: &Bolt11Invoice,
662 ) -> anyhow::Result<FinalReceiveState>;
663}