1use core::fmt;
2use std::collections::BTreeMap;
3
4use anyhow::anyhow;
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::FmtCompactAnyhow;
15use fedimint_core::{NumPeersExt, OutPoint, PeerId};
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 tpe::{AggregatePublicKey, DecryptionKeyShare, PublicKeyShare, aggregate_dk_shares};
21use tracing::warn;
22
23use super::events::{IncomingPaymentFailed, IncomingPaymentSucceeded};
24use crate::GatewayClientContextV2;
25
26#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
27pub struct ReceiveStateMachine {
28 pub common: ReceiveSMCommon,
29 pub state: ReceiveSMState,
30}
31
32impl ReceiveStateMachine {
33 pub fn update(&self, state: ReceiveSMState) -> Self {
34 Self {
35 common: self.common.clone(),
36 state,
37 }
38 }
39}
40
41impl fmt::Display for ReceiveStateMachine {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(
44 f,
45 "Receive State Machine Operation ID: {:?} State: {}",
46 self.common.operation_id, self.state
47 )
48 }
49}
50
51#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
52pub struct ReceiveSMCommon {
53 pub operation_id: OperationId,
54 pub contract: IncomingContract,
55 pub outpoint: OutPoint,
56 pub refund_keypair: Keypair,
57}
58
59#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
60pub enum ReceiveSMState {
61 Funding,
62 Rejected(String),
63 Success([u8; 32]),
64 Failure,
65 Refunding(Vec<OutPoint>),
66}
67
68impl fmt::Display for ReceiveSMState {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 ReceiveSMState::Funding => write!(f, "Funding"),
72 ReceiveSMState::Rejected(_) => write!(f, "Rejected"),
73 ReceiveSMState::Success(_) => write!(f, "Success"),
74 ReceiveSMState::Failure => write!(f, "Failure"),
75 ReceiveSMState::Refunding(_) => write!(f, "Refunding"),
76 }
77 }
78}
79
80#[cfg_attr(doc, aquamarine::aquamarine)]
81impl State for ReceiveStateMachine {
93 type ModuleContext = GatewayClientContextV2;
94
95 fn transitions(
96 &self,
97 context: &Self::ModuleContext,
98 global_context: &DynGlobalClientContext,
99 ) -> Vec<StateTransition<Self>> {
100 let gc = global_context.clone();
101 let tpe_agg_pk = context.tpe_agg_pk;
102 let gateway_context_ready = context.clone();
103
104 match &self.state {
105 ReceiveSMState::Funding => {
106 vec![StateTransition::new(
107 Self::await_decryption_shares(
108 global_context.clone(),
109 context.tpe_pks.clone(),
110 self.common.outpoint,
111 self.common.contract.clone(),
112 ),
113 move |dbtx, output_outcomes, old_state| {
114 Box::pin(Self::transition_decryption_shares(
115 dbtx,
116 output_outcomes,
117 old_state,
118 gc.clone(),
119 tpe_agg_pk,
120 gateway_context_ready.clone(),
121 ))
122 },
123 )]
124 }
125 ReceiveSMState::Success(..)
126 | ReceiveSMState::Rejected(..)
127 | ReceiveSMState::Refunding(..)
128 | ReceiveSMState::Failure => {
129 vec![]
130 }
131 }
132 }
133
134 fn operation_id(&self) -> OperationId {
135 self.common.operation_id
136 }
137}
138
139impl ReceiveStateMachine {
140 async fn await_decryption_shares(
141 global_context: DynGlobalClientContext,
142 tpe_pks: BTreeMap<PeerId, PublicKeyShare>,
143 outpoint: OutPoint,
144 contract: IncomingContract,
145 ) -> Result<BTreeMap<PeerId, DecryptionKeyShare>, String> {
146 let num_peers = global_context.api().all_peers().to_num_peers();
147 let module_api = global_context.module_api();
148
149 let decryption_shares = module_api.request_with_strategy_retry(
155 FilterMapThreshold::new(
156 move |peer_id, share: DecryptionKeyShare| {
157 if !contract.verify_decryption_share(
158 tpe_pks
159 .get(&peer_id)
160 .ok_or(ServerError::InternalClientError(anyhow!(
161 "Missing TPE PK for peer {peer_id}?!"
162 )))?,
163 &share,
164 ) {
165 return Err(fedimint_api_client::api::ServerError::InvalidResponse(
166 anyhow!("Invalid decryption share"),
167 ));
168 }
169
170 Ok(share)
171 },
172 num_peers,
173 ),
174 DECRYPTION_KEY_SHARE_ENDPOINT.to_owned(),
175 ApiRequestErased::new(outpoint),
176 );
177
178 let decryption_shares = std::pin::pin!(decryption_shares);
179 let tx_accepted = std::pin::pin!(global_context.await_tx_accepted(outpoint.txid));
180
181 match futures::future::select(decryption_shares, tx_accepted).await {
182 futures::future::Either::Left((shares, _)) => Ok(shares),
183 futures::future::Either::Right((accepted, decryption_shares)) => {
184 accepted?;
185
186 Ok(decryption_shares.await)
187 }
188 }
189 }
190
191 async fn transition_decryption_shares(
192 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
193 decryption_shares: Result<BTreeMap<PeerId, DecryptionKeyShare>, String>,
194 old_state: ReceiveStateMachine,
195 global_context: DynGlobalClientContext,
196 tpe_agg_pk: AggregatePublicKey,
197 client_ctx: GatewayClientContextV2,
198 ) -> ReceiveStateMachine {
199 let decryption_shares = match decryption_shares {
200 Ok(decryption_shares) => decryption_shares
201 .into_iter()
202 .map(|(peer, share)| (peer.to_usize() as u64, share))
203 .collect(),
204 Err(error) => {
205 client_ctx
206 .module
207 .client_ctx
208 .log_event(
209 &mut dbtx.module_tx(),
210 IncomingPaymentFailed {
211 payment_image: old_state
212 .common
213 .contract
214 .commitment
215 .payment_image
216 .clone(),
217 error: error.clone(),
218 },
219 )
220 .await;
221
222 return old_state.update(ReceiveSMState::Rejected(error));
223 }
224 };
225
226 let agg_decryption_key = aggregate_dk_shares(&decryption_shares);
227
228 if !old_state
229 .common
230 .contract
231 .verify_agg_decryption_key(&tpe_agg_pk, &agg_decryption_key)
232 {
233 warn!(target: LOG_CLIENT_MODULE_GW, "Failed to obtain decryption key. Client config's public keys are inconsistent");
234
235 client_ctx
236 .module
237 .client_ctx
238 .log_event(
239 &mut dbtx.module_tx(),
240 IncomingPaymentFailed {
241 payment_image: old_state.common.contract.commitment.payment_image.clone(),
242 error: "Client config's public keys are inconsistent".to_string(),
243 },
244 )
245 .await;
246
247 return old_state.update(ReceiveSMState::Failure);
248 }
249
250 if let Some(preimage) = old_state
251 .common
252 .contract
253 .decrypt_preimage(&agg_decryption_key)
254 {
255 client_ctx
256 .module
257 .client_ctx
258 .log_event(
259 &mut dbtx.module_tx(),
260 IncomingPaymentSucceeded {
261 payment_image: old_state.common.contract.commitment.payment_image.clone(),
262 },
263 )
264 .await;
265
266 return old_state.update(ReceiveSMState::Success(preimage));
267 }
268
269 let client_input = ClientInput::<LightningInput> {
270 input: LightningInput::V0(LightningInputV0::Incoming(
271 old_state.common.outpoint,
272 agg_decryption_key,
273 )),
274 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
275 keys: vec![old_state.common.refund_keypair],
276 };
277
278 let outpoints = match global_context
279 .claim_inputs(
280 dbtx,
281 ClientInputBundle::new_no_sm(vec![client_input]),
283 )
284 .await
285 {
286 Ok(outpoints) => outpoints.into_iter().collect(),
287 Err(err) => {
294 warn!(
295 target: LOG_CLIENT_MODULE_GW,
296 err = %err.fmt_compact_anyhow(),
297 amount = %old_state.common.contract.commitment.amount,
298 "Not refunding incoming contract, its amount does not cover the refund fee"
299 );
300
301 client_ctx
302 .module
303 .client_ctx
304 .log_event(
305 &mut dbtx.module_tx(),
306 IncomingPaymentFailed {
307 payment_image: old_state
308 .common
309 .contract
310 .commitment
311 .payment_image
312 .clone(),
313 error: "Contract does not cover the refund fee".to_string(),
314 },
315 )
316 .await;
317
318 return old_state.update(ReceiveSMState::Failure);
319 }
320 };
321
322 client_ctx
323 .module
324 .client_ctx
325 .log_event(
326 &mut dbtx.module_tx(),
327 IncomingPaymentFailed {
328 payment_image: old_state.common.contract.commitment.payment_image.clone(),
329 error: "Failed to decrypt preimage".to_string(),
330 },
331 )
332 .await;
333
334 old_state.update(ReceiveSMState::Refunding(outpoints))
335 }
336}