Skip to main content

fedimint_lnv2_client/
send_sm.rs

1use bitcoin::hashes::sha256;
2use fedimint_api_client::api::ServerError;
3use fedimint_client_module::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
6use fedimint_core::config::FederationId;
7use fedimint_core::core::OperationId;
8use fedimint_core::encoding::{Decodable, Encodable};
9use fedimint_core::module::Amounts;
10use fedimint_core::util::SafeUrl;
11use fedimint_core::util::backoff_util::api_networking_backoff;
12use fedimint_core::{OutPoint, TransactionId, crit, secp256k1, util};
13use fedimint_lnv2_common::contracts::OutgoingContract;
14use fedimint_lnv2_common::{LightningInput, LightningInputV0, OutgoingWitness};
15use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
16use futures::future::pending;
17use secp256k1::Keypair;
18use secp256k1::schnorr::Signature;
19use tracing::instrument;
20
21use crate::api::LightningFederationApi;
22use crate::events::{SendPaymentStatus, SendPaymentUpdateEvent};
23use crate::{LightningClientContext, LightningInvoice};
24
25#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
26pub struct SendStateMachine {
27    pub common: SendSMCommon,
28    pub state: SendSMState,
29}
30
31impl SendStateMachine {
32    pub fn update(&self, state: SendSMState) -> Self {
33        Self {
34            common: self.common.clone(),
35            state,
36        }
37    }
38}
39
40#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
41pub struct SendSMCommon {
42    pub operation_id: OperationId,
43    pub outpoint: OutPoint,
44    pub contract: OutgoingContract,
45    pub gateway_api: Option<SafeUrl>,
46    pub invoice: Option<LightningInvoice>,
47    pub refund_keypair: Keypair,
48}
49
50#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
51pub enum SendSMState {
52    Funding,
53    Funded,
54    Rejected(String),
55    Success([u8; 32]),
56    Refunding(Vec<OutPoint>),
57}
58
59async fn send_update_event(
60    context: LightningClientContext,
61    dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
62    operation_id: OperationId,
63    status: SendPaymentStatus,
64) {
65    context
66        .client_ctx
67        .log_event(
68            &mut dbtx.module_tx(),
69            SendPaymentUpdateEvent {
70                operation_id,
71                status,
72            },
73        )
74        .await;
75}
76
77#[cfg_attr(doc, aquamarine::aquamarine)]
78/// State machine that requests the lightning gateway to pay an invoice on
79/// behalf of a federation client.
80///
81/// ```mermaid
82/// graph LR
83/// classDef virtual fill:#fff,stroke-dasharray: 5 5
84///
85///     Funding -- funding tx is rejected --> Rejected
86///     Funding -- funding tx is accepted --> Funded
87///     Funded -- post invoice returns preimage  --> Success
88///     Funded -- post invoice returns forfeit tx --> Refunding
89///     Funded -- await_preimage returns preimage --> Success
90///     Funded -- await_preimage expires --> Refunding
91/// ```
92impl State for SendStateMachine {
93    type ModuleContext = LightningClientContext;
94
95    fn transitions(
96        &self,
97        context: &Self::ModuleContext,
98        global_context: &DynGlobalClientContext,
99    ) -> Vec<StateTransition<Self>> {
100        let c_pay = context.clone();
101        let gc_pay = global_context.clone();
102        let c_preimage = context.clone();
103        let gc_preimage = global_context.clone();
104
105        match &self.state {
106            SendSMState::Funding => {
107                vec![StateTransition::new(
108                    Self::await_funding(global_context.clone(), self.common.outpoint.txid),
109                    move |_, error, old_state| {
110                        Box::pin(async move { Self::transition_funding(error, &old_state) })
111                    },
112                )]
113            }
114            SendSMState::Funded => {
115                vec![
116                    StateTransition::new(
117                        Self::gateway_send_payment(
118                            self.common.gateway_api.clone().unwrap(),
119                            context.federation_id,
120                            self.common.outpoint,
121                            self.common.contract.clone(),
122                            self.common.invoice.clone().unwrap(),
123                            self.common.refund_keypair,
124                            context.clone(),
125                        ),
126                        move |dbtx, response, old_state| {
127                            Box::pin(Self::transition_gateway_send_payment(
128                                c_pay.clone(),
129                                gc_pay.clone(),
130                                dbtx,
131                                response,
132                                old_state,
133                            ))
134                        },
135                    ),
136                    StateTransition::new(
137                        Self::await_preimage(
138                            self.common.outpoint,
139                            self.common.contract.clone(),
140                            gc_preimage.clone(),
141                        ),
142                        move |dbtx, preimage, old_state| {
143                            Box::pin(Self::transition_preimage(
144                                c_preimage.clone(),
145                                dbtx,
146                                gc_preimage.clone(),
147                                old_state,
148                                preimage,
149                            ))
150                        },
151                    ),
152                ]
153            }
154            SendSMState::Refunding(..) | SendSMState::Success(..) | SendSMState::Rejected(..) => {
155                vec![]
156            }
157        }
158    }
159
160    fn operation_id(&self) -> OperationId {
161        self.common.operation_id
162    }
163}
164
165impl SendStateMachine {
166    async fn await_funding(
167        global_context: DynGlobalClientContext,
168        txid: TransactionId,
169    ) -> Result<(), String> {
170        global_context.await_tx_accepted(txid).await
171    }
172
173    fn transition_funding(
174        result: Result<(), String>,
175        old_state: &SendStateMachine,
176    ) -> SendStateMachine {
177        match result {
178            Ok(()) => old_state.update(SendSMState::Funded),
179            Err(error) => old_state.update(SendSMState::Rejected(error)),
180        }
181    }
182
183    #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(refund_keypair, context))]
184    async fn gateway_send_payment(
185        gateway_api: SafeUrl,
186        federation_id: FederationId,
187        outpoint: OutPoint,
188        contract: OutgoingContract,
189        invoice: LightningInvoice,
190        refund_keypair: Keypair,
191        context: LightningClientContext,
192    ) -> Result<[u8; 32], Signature> {
193        util::retry("gateway-send-payment", api_networking_backoff(), || async {
194            let payment_result = context
195                .gateway_conn
196                .send_payment(
197                    gateway_api.clone(),
198                    federation_id,
199                    outpoint,
200                    contract.clone(),
201                    invoice.clone(),
202                    refund_keypair.sign_schnorr(secp256k1::Message::from_digest(
203                        *invoice.consensus_hash::<sha256::Hash>().as_ref(),
204                    )),
205                )
206                .await?;
207
208            if !contract.verify_gateway_response(&payment_result) {
209                return Err(SendPaymentAttemptError::InvalidResponse(payment_result));
210            }
211
212            Ok(payment_result)
213        })
214        .await
215        .expect("Number of retries has no limit")
216    }
217
218    async fn transition_gateway_send_payment(
219        context: LightningClientContext,
220        global_context: DynGlobalClientContext,
221        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
222        gateway_response: Result<[u8; 32], Signature>,
223        old_state: SendStateMachine,
224    ) -> SendStateMachine {
225        match gateway_response {
226            Ok(preimage) => {
227                send_update_event(
228                    context,
229                    dbtx,
230                    old_state.common.operation_id,
231                    SendPaymentStatus::Success(preimage),
232                )
233                .await;
234
235                old_state.update(SendSMState::Success(preimage))
236            }
237            Err(signature) => {
238                let client_input = ClientInput::<LightningInput> {
239                    input: LightningInput::V0(LightningInputV0::Outgoing(
240                        old_state.common.outpoint,
241                        OutgoingWitness::Cancel(signature),
242                    )),
243                    amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
244                    keys: vec![old_state.common.refund_keypair],
245                };
246
247                let change_range = global_context
248                    .claim_inputs(
249                        dbtx,
250                        // The input of the refund tx is managed by this state machine
251                        ClientInputBundle::new_no_sm(vec![client_input]),
252                    )
253                    .await
254                    .expect("Cannot claim input, additional funding needed");
255
256                send_update_event(
257                    context,
258                    dbtx,
259                    old_state.common.operation_id,
260                    SendPaymentStatus::Refunded,
261                )
262                .await;
263
264                old_state.update(SendSMState::Refunding(change_range.into_iter().collect()))
265            }
266        }
267    }
268
269    #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(global_context))]
270    async fn await_preimage(
271        outpoint: OutPoint,
272        contract: OutgoingContract,
273        global_context: DynGlobalClientContext,
274    ) -> Option<[u8; 32]> {
275        let preimage = global_context
276            .module_api()
277            .await_preimage(outpoint, contract.expiration)
278            .await?;
279
280        if contract.verify_preimage(&preimage) {
281            return Some(preimage);
282        }
283
284        crit!(target: LOG_CLIENT_MODULE_LNV2, "Federation returned invalid preimage {:?}", preimage);
285
286        pending().await
287    }
288
289    async fn transition_preimage(
290        context: LightningClientContext,
291        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
292        global_context: DynGlobalClientContext,
293        old_state: SendStateMachine,
294        preimage: Option<[u8; 32]>,
295    ) -> SendStateMachine {
296        if let Some(preimage) = preimage {
297            send_update_event(
298                context,
299                dbtx,
300                old_state.common.operation_id,
301                SendPaymentStatus::Success(preimage),
302            )
303            .await;
304
305            return old_state.update(SendSMState::Success(preimage));
306        }
307
308        let client_input = ClientInput::<LightningInput> {
309            input: LightningInput::V0(LightningInputV0::Outgoing(
310                old_state.common.outpoint,
311                OutgoingWitness::Refund,
312            )),
313            amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
314            keys: vec![old_state.common.refund_keypair],
315        };
316
317        let change_range = global_context
318            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
319            .await
320            .expect("Cannot claim input, additional funding needed");
321
322        send_update_event(
323            context,
324            dbtx,
325            old_state.common.operation_id,
326            SendPaymentStatus::Refunded,
327        )
328        .await;
329
330        old_state.update(SendSMState::Refunding(change_range.into_iter().collect()))
331    }
332}
333
334/// Why one attempt to have the gateway pay the invoice failed, so that it is
335/// made again.
336#[derive(Debug, thiserror::Error)]
337enum SendPaymentAttemptError {
338    /// The gateway could not be reached, or answered with an error.
339    #[error(transparent)]
340    Gateway(#[from] ServerError),
341
342    /// The gateway's answer neither proves the payment nor cancels the
343    /// contract.
344    #[error("Invalid gateway response: {0:?}")]
345    InvalidResponse(Result<[u8; 32], Signature>),
346}