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