1use core::fmt;
2use std::collections::BTreeMap;
3use std::sync::Arc;
4
5use fedimint_api_client::api::{FederationApiExt, ServerError};
6use fedimint_api_client::query::FilterMapThreshold;
7use fedimint_client_module::DynGlobalClientContext;
8use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
9use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
10use fedimint_core::core::OperationId;
11use fedimint_core::encoding::{Decodable, Encodable};
12use fedimint_core::module::{Amounts, ApiRequestErased};
13use fedimint_core::secp256k1::Keypair;
14use fedimint_core::util::FmtCompact;
15use fedimint_core::{NumPeersExt, OutPoint, PeerId, runtime};
16use fedimint_lnv2_common::contracts::IncomingContract;
17use fedimint_lnv2_common::endpoint_constants::DECRYPTION_KEY_SHARE_ENDPOINT;
18use fedimint_lnv2_common::{LightningInput, LightningInputV0};
19use fedimint_logging::LOG_CLIENT_MODULE_GW;
20use serde::{Deserialize, Serialize};
21use tpe::{
22 AggregateDecryptionKey, AggregatePublicKey, DecryptionKeyShare, PublicKeyShare,
23 aggregate_dk_shares,
24};
25use tracing::warn;
26
27use super::events::{IncomingPaymentFailed, IncomingPaymentSucceeded};
28use crate::GatewayClientContextV2;
29
30#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
31pub struct ReceiveStateMachine {
32 pub common: ReceiveSMCommon,
33 pub state: ReceiveSMState,
34}
35
36impl ReceiveStateMachine {
37 pub fn update(&self, state: ReceiveSMState) -> Self {
38 Self {
39 common: self.common.clone(),
40 state,
41 }
42 }
43}
44
45impl fmt::Display for ReceiveStateMachine {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(
48 f,
49 "Receive State Machine Operation ID: {:?} State: {}",
50 self.common.operation_id, self.state
51 )
52 }
53}
54
55#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
56pub struct ReceiveSMCommon {
57 pub operation_id: OperationId,
58 pub contract: IncomingContract,
59 pub outpoint: OutPoint,
60 pub refund_keypair: Keypair,
61}
62
63#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
64pub enum ReceiveSMState {
65 Funding,
66 Rejected(String),
67 Success([u8; 32]),
68 Failure,
69 Refunding(Vec<OutPoint>),
70}
71
72impl fmt::Display for ReceiveSMState {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 ReceiveSMState::Funding => write!(f, "Funding"),
76 ReceiveSMState::Rejected(_) => write!(f, "Rejected"),
77 ReceiveSMState::Success(_) => write!(f, "Success"),
78 ReceiveSMState::Failure => write!(f, "Failure"),
79 ReceiveSMState::Refunding(_) => write!(f, "Refunding"),
80 }
81 }
82}
83
84#[derive(Debug, Serialize, Deserialize)]
89pub enum DecryptionOutcome {
90 Rejected(String),
92 InconsistentKeys,
95 Decrypted(AggregateDecryptionKey, Option<[u8; 32]>),
98}
99
100impl State for ReceiveStateMachine {
112 type ModuleContext = GatewayClientContextV2;
113
114 fn transitions(
115 &self,
116 context: &Self::ModuleContext,
117 global_context: &DynGlobalClientContext,
118 ) -> Vec<StateTransition<Self>> {
119 let gc = global_context.clone();
120 let tpe_agg_pk = context.tpe_agg_pk;
121 let gateway_context_ready = context.clone();
122
123 match &self.state {
124 ReceiveSMState::Funding => {
125 vec![StateTransition::new(
126 Self::await_decryption_shares(
127 global_context.clone(),
128 context.tpe_pks.clone(),
129 tpe_agg_pk,
130 self.common.outpoint,
131 self.common.contract.clone(),
132 ),
133 move |dbtx, outcome, old_state| {
134 Box::pin(Self::transition_decryption_shares(
135 dbtx,
136 outcome,
137 old_state,
138 gc.clone(),
139 gateway_context_ready.clone(),
140 ))
141 },
142 )]
143 }
144 ReceiveSMState::Success(..)
145 | ReceiveSMState::Rejected(..)
146 | ReceiveSMState::Refunding(..)
147 | ReceiveSMState::Failure => {
148 vec![]
149 }
150 }
151 }
152
153 fn operation_id(&self) -> OperationId {
154 self.common.operation_id
155 }
156}
157
158impl ReceiveStateMachine {
159 async fn await_decryption_shares(
160 global_context: DynGlobalClientContext,
161 tpe_pks: BTreeMap<PeerId, PublicKeyShare>,
162 tpe_agg_pk: AggregatePublicKey,
163 outpoint: OutPoint,
164 contract: IncomingContract,
165 ) -> DecryptionOutcome {
166 let num_peers = global_context.api().all_peers().to_num_peers();
167 let module_api = global_context.module_api();
168 let tpe_pks = Arc::new(tpe_pks);
169 let contract = Arc::new(contract);
170
171 let decryption_shares = module_api.request_with_strategy_retry(
177 FilterMapThreshold::new(
178 {
179 let tpe_pks = tpe_pks.clone();
180 let contract = contract.clone();
181
182 move |peer_id, share: DecryptionKeyShare| {
183 let tpe_pks = tpe_pks.clone();
184 let contract = contract.clone();
185
186 runtime::spawn_blocking(move || {
188 let pk =
189 tpe_pks
190 .get(&peer_id)
191 .ok_or(ServerError::InternalClientError(format!(
192 "Missing TPE PK for peer {peer_id}?!"
193 )))?;
194
195 if !contract.verify_decryption_share(pk, &share) {
196 return Err(ServerError::InvalidResponse(
197 "Invalid decryption share".to_string(),
198 ));
199 }
200
201 Ok(share)
202 })
203 }
204 },
205 num_peers,
206 ),
207 DECRYPTION_KEY_SHARE_ENDPOINT.to_owned(),
208 ApiRequestErased::new(outpoint),
209 );
210
211 let decryption_shares = std::pin::pin!(decryption_shares);
212 let tx_accepted = std::pin::pin!(global_context.await_tx_accepted(outpoint.txid));
213
214 let decryption_shares = match futures::future::select(decryption_shares, tx_accepted).await
215 {
216 futures::future::Either::Left((shares, _)) => shares,
217 futures::future::Either::Right((accepted, decryption_shares)) => {
218 if let Err(error) = accepted {
219 return DecryptionOutcome::Rejected(error);
220 }
221
222 decryption_shares.await
223 }
224 };
225
226 runtime::spawn_blocking(move || {
228 let agg_decryption_key = aggregate_dk_shares(
229 &decryption_shares
230 .into_iter()
231 .map(|(peer, share)| (peer.to_usize() as u64, share))
232 .collect(),
233 );
234
235 if !contract.verify_agg_decryption_key(&tpe_agg_pk, &agg_decryption_key) {
236 return DecryptionOutcome::InconsistentKeys;
237 }
238
239 let preimage = contract.decrypt_preimage(&agg_decryption_key);
240
241 DecryptionOutcome::Decrypted(agg_decryption_key, preimage)
242 })
243 .await
244 }
245
246 async fn transition_decryption_shares(
247 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
248 outcome: DecryptionOutcome,
249 old_state: ReceiveStateMachine,
250 global_context: DynGlobalClientContext,
251 client_ctx: GatewayClientContextV2,
252 ) -> ReceiveStateMachine {
253 let payment_image = old_state.common.contract.commitment.payment_image.clone();
254
255 let (agg_decryption_key, preimage) = match outcome {
256 DecryptionOutcome::Rejected(error) => {
257 client_ctx
258 .module
259 .client_ctx
260 .log_event(
261 &mut dbtx.module_tx(),
262 IncomingPaymentFailed {
263 payment_image,
264 error: error.clone(),
265 },
266 )
267 .await;
268
269 return old_state.update(ReceiveSMState::Rejected(error));
270 }
271 DecryptionOutcome::InconsistentKeys => {
272 warn!(target: LOG_CLIENT_MODULE_GW, "Failed to obtain decryption key. Client config's public keys are inconsistent");
273
274 client_ctx
275 .module
276 .client_ctx
277 .log_event(
278 &mut dbtx.module_tx(),
279 IncomingPaymentFailed {
280 payment_image,
281 error: "Client config's public keys are inconsistent".to_string(),
282 },
283 )
284 .await;
285
286 return old_state.update(ReceiveSMState::Failure);
287 }
288 DecryptionOutcome::Decrypted(agg_decryption_key, preimage) => {
289 (agg_decryption_key, preimage)
290 }
291 };
292
293 if let Some(preimage) = preimage {
294 client_ctx
295 .module
296 .client_ctx
297 .log_event(
298 &mut dbtx.module_tx(),
299 IncomingPaymentSucceeded { payment_image },
300 )
301 .await;
302
303 return old_state.update(ReceiveSMState::Success(preimage));
304 }
305
306 let client_input = ClientInput::<LightningInput> {
307 input: LightningInput::V0(LightningInputV0::Incoming(
308 old_state.common.outpoint,
309 agg_decryption_key,
310 )),
311 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
312 keys: vec![old_state.common.refund_keypair],
313 };
314
315 let outpoints = match global_context
316 .claim_inputs(
317 dbtx,
318 ClientInputBundle::new_no_sm(vec![client_input]),
320 )
321 .await
322 {
323 Ok(outpoints) => outpoints.into_iter().collect(),
324 Err(err) => {
331 warn!(
332 target: LOG_CLIENT_MODULE_GW,
333 err = %err.fmt_compact(),
334 amount = %old_state.common.contract.commitment.amount,
335 "Not refunding incoming contract, its amount does not cover the refund fee"
336 );
337
338 client_ctx
339 .module
340 .client_ctx
341 .log_event(
342 &mut dbtx.module_tx(),
343 IncomingPaymentFailed {
344 payment_image: old_state
345 .common
346 .contract
347 .commitment
348 .payment_image
349 .clone(),
350 error: "Contract does not cover the refund fee".to_string(),
351 },
352 )
353 .await;
354
355 return old_state.update(ReceiveSMState::Failure);
356 }
357 };
358
359 client_ctx
360 .module
361 .client_ctx
362 .log_event(
363 &mut dbtx.module_tx(),
364 IncomingPaymentFailed {
365 payment_image: old_state.common.contract.commitment.payment_image.clone(),
366 error: "Failed to decrypt preimage".to_string(),
367 },
368 )
369 .await;
370
371 old_state.update(ReceiveSMState::Refunding(outpoints))
372 }
373}