fedimint_ln_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::module_name_repetitions)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::too_many_lines)]
6
7pub mod db;
8use std::collections::{BTreeMap, BTreeSet};
9use std::time::Duration;
10
11use anyhow::{Context, bail};
12use bitcoin_hashes::{Hash as BitcoinHash, sha256};
13use fedimint_core::config::{
14    ConfigGenModuleParams, ServerModuleConfig, ServerModuleConsensusConfig,
15    TypedServerModuleConfig, TypedServerModuleConsensusConfig,
16};
17use fedimint_core::core::ModuleInstanceId;
18use fedimint_core::db::{DatabaseTransaction, DatabaseValue, IDatabaseTransactionOpsCoreTyped};
19use fedimint_core::encoding::Encodable;
20use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
21use fedimint_core::module::audit::Audit;
22use fedimint_core::module::{
23    ApiEndpoint, ApiEndpointContext, ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion,
24    InputMeta, ModuleConsensusVersion, ModuleInit, SupportedModuleApiVersions,
25    TransactionItemAmount, api_endpoint,
26};
27use fedimint_core::secp256k1::{Message, PublicKey, SECP256K1};
28use fedimint_core::task::sleep;
29use fedimint_core::{
30    Amount, InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send,
31    push_db_pair_items,
32};
33pub use fedimint_ln_common as common;
34use fedimint_ln_common::config::{
35    FeeConsensus, LightningClientConfig, LightningConfig, LightningConfigConsensus,
36    LightningConfigPrivate, LightningGenParams,
37};
38use fedimint_ln_common::contracts::incoming::{IncomingContractAccount, IncomingContractOffer};
39use fedimint_ln_common::contracts::{
40    Contract, ContractId, ContractOutcome, DecryptedPreimage, DecryptedPreimageStatus,
41    EncryptedPreimage, FundedContract, IdentifiableContract, Preimage, PreimageDecryptionShare,
42    PreimageKey,
43};
44use fedimint_ln_common::federation_endpoint_constants::{
45    ACCOUNT_ENDPOINT, AWAIT_ACCOUNT_ENDPOINT, AWAIT_BLOCK_HEIGHT_ENDPOINT, AWAIT_OFFER_ENDPOINT,
46    AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT, AWAIT_PREIMAGE_DECRYPTION, BLOCK_COUNT_ENDPOINT,
47    GET_DECRYPTED_PREIMAGE_STATUS, LIST_GATEWAYS_ENDPOINT, OFFER_ENDPOINT,
48    REGISTER_GATEWAY_ENDPOINT, REMOVE_GATEWAY_CHALLENGE_ENDPOINT, REMOVE_GATEWAY_ENDPOINT,
49};
50use fedimint_ln_common::{
51    ContractAccount, LightningCommonInit, LightningConsensusItem, LightningGatewayAnnouncement,
52    LightningGatewayRegistration, LightningInput, LightningInputError, LightningModuleTypes,
53    LightningOutput, LightningOutputError, LightningOutputOutcome, LightningOutputOutcomeV0,
54    LightningOutputV0, MODULE_CONSENSUS_VERSION, RemoveGatewayRequest,
55    create_gateway_remove_message,
56};
57use fedimint_logging::LOG_MODULE_LN;
58use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
59use fedimint_server_core::config::PeerHandleOps;
60use fedimint_server_core::{ServerModule, ServerModuleInit, ServerModuleInitArgs};
61use futures::StreamExt;
62use metrics::{LN_CANCEL_OUTGOING_CONTRACTS, LN_FUNDED_CONTRACT_SATS, LN_INCOMING_OFFER};
63use rand::rngs::OsRng;
64use strum::IntoEnumIterator;
65use threshold_crypto::poly::Commitment;
66use threshold_crypto::serde_impl::SerdeSecret;
67use threshold_crypto::{PublicKeySet, SecretKeyShare};
68use tracing::{debug, error, info, info_span, trace, warn};
69
70use crate::db::{
71    AgreedDecryptionShareContractIdPrefix, AgreedDecryptionShareKey,
72    AgreedDecryptionShareKeyPrefix, BlockCountVoteKey, BlockCountVotePrefix, ContractKey,
73    ContractKeyPrefix, ContractUpdateKey, ContractUpdateKeyPrefix, DbKeyPrefix,
74    EncryptedPreimageIndexKey, EncryptedPreimageIndexKeyPrefix, LightningAuditItemKey,
75    LightningAuditItemKeyPrefix, LightningGatewayKey, LightningGatewayKeyPrefix, OfferKey,
76    OfferKeyPrefix, ProposeDecryptionShareKey, ProposeDecryptionShareKeyPrefix,
77};
78
79mod metrics;
80
81#[derive(Debug, Clone)]
82pub struct LightningInit;
83
84impl ModuleInit for LightningInit {
85    type Common = LightningCommonInit;
86
87    async fn dump_database(
88        &self,
89        dbtx: &mut DatabaseTransaction<'_>,
90        prefix_names: Vec<String>,
91    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
92        let mut lightning: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
93            BTreeMap::new();
94        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
95            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
96        });
97        for table in filtered_prefixes {
98            match table {
99                DbKeyPrefix::AgreedDecryptionShare => {
100                    push_db_pair_items!(
101                        dbtx,
102                        AgreedDecryptionShareKeyPrefix,
103                        AgreedDecryptionShareKey,
104                        PreimageDecryptionShare,
105                        lightning,
106                        "Accepted Decryption Shares"
107                    );
108                }
109                DbKeyPrefix::Contract => {
110                    push_db_pair_items!(
111                        dbtx,
112                        ContractKeyPrefix,
113                        ContractKey,
114                        ContractAccount,
115                        lightning,
116                        "Contracts"
117                    );
118                }
119                DbKeyPrefix::ContractUpdate => {
120                    push_db_pair_items!(
121                        dbtx,
122                        ContractUpdateKeyPrefix,
123                        ContractUpdateKey,
124                        LightningOutputOutcomeV0,
125                        lightning,
126                        "Contract Updates"
127                    );
128                }
129                DbKeyPrefix::LightningGateway => {
130                    push_db_pair_items!(
131                        dbtx,
132                        LightningGatewayKeyPrefix,
133                        LightningGatewayKey,
134                        LightningGatewayRegistration,
135                        lightning,
136                        "Lightning Gateways"
137                    );
138                }
139                DbKeyPrefix::Offer => {
140                    push_db_pair_items!(
141                        dbtx,
142                        OfferKeyPrefix,
143                        OfferKey,
144                        IncomingContractOffer,
145                        lightning,
146                        "Offers"
147                    );
148                }
149                DbKeyPrefix::ProposeDecryptionShare => {
150                    push_db_pair_items!(
151                        dbtx,
152                        ProposeDecryptionShareKeyPrefix,
153                        ProposeDecryptionShareKey,
154                        PreimageDecryptionShare,
155                        lightning,
156                        "Proposed Decryption Shares"
157                    );
158                }
159                DbKeyPrefix::BlockCountVote => {
160                    push_db_pair_items!(
161                        dbtx,
162                        BlockCountVotePrefix,
163                        BlockCountVoteKey,
164                        u64,
165                        lightning,
166                        "Block Count Votes"
167                    );
168                }
169                DbKeyPrefix::EncryptedPreimageIndex => {
170                    push_db_pair_items!(
171                        dbtx,
172                        EncryptedPreimageIndexKeyPrefix,
173                        EncryptedPreimageIndexKey,
174                        (),
175                        lightning,
176                        "Encrypted Preimage Hashes"
177                    );
178                }
179                DbKeyPrefix::LightningAuditItem => {
180                    push_db_pair_items!(
181                        dbtx,
182                        LightningAuditItemKeyPrefix,
183                        LightningAuditItemKey,
184                        Amount,
185                        lightning,
186                        "Lightning Audit Items"
187                    );
188                }
189            }
190        }
191
192        Box::new(lightning.into_iter())
193    }
194}
195
196#[apply(async_trait_maybe_send!)]
197impl ServerModuleInit for LightningInit {
198    type Module = Lightning;
199    type Params = LightningGenParams;
200
201    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
202        &[MODULE_CONSENSUS_VERSION]
203    }
204
205    fn supported_api_versions(&self) -> SupportedModuleApiVersions {
206        SupportedModuleApiVersions::from_raw(
207            (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
208            (
209                MODULE_CONSENSUS_VERSION.major,
210                MODULE_CONSENSUS_VERSION.minor,
211            ),
212            &[(0, 1)],
213        )
214    }
215
216    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
217        // Eagerly initialize metrics that trigger infrequently
218        LN_CANCEL_OUTGOING_CONTRACTS.get();
219
220        Ok(Lightning {
221            cfg: args.cfg().to_typed()?,
222            our_peer_id: args.our_peer_id(),
223            server_bitcoin_rpc_monitor: args.server_bitcoin_rpc_monitor(),
224        })
225    }
226
227    fn trusted_dealer_gen(
228        &self,
229        peers: &[PeerId],
230        params: &ConfigGenModuleParams,
231    ) -> BTreeMap<PeerId, ServerModuleConfig> {
232        let params = self.parse_params(params).unwrap();
233        let sks = threshold_crypto::SecretKeySet::random(peers.to_num_peers().degree(), &mut OsRng);
234        let pks = sks.public_keys();
235
236        peers
237            .iter()
238            .map(|&peer| {
239                let sk = sks.secret_key_share(peer.to_usize());
240
241                (
242                    peer,
243                    LightningConfig {
244                        consensus: LightningConfigConsensus {
245                            threshold_pub_keys: pks.clone(),
246                            fee_consensus: FeeConsensus::default(),
247                            network: NetworkLegacyEncodingWrapper(params.consensus.network),
248                        },
249                        private: LightningConfigPrivate {
250                            threshold_sec_key: threshold_crypto::serde_impl::SerdeSecret(sk),
251                        },
252                    }
253                    .to_erased(),
254                )
255            })
256            .collect()
257    }
258
259    async fn distributed_gen(
260        &self,
261        peers: &(dyn PeerHandleOps + Send + Sync),
262        params: &ConfigGenModuleParams,
263    ) -> anyhow::Result<ServerModuleConfig> {
264        let params = self.parse_params(params).unwrap();
265
266        let (polynomial, mut sks) = peers.run_dkg_g1().await?;
267
268        let server = LightningConfig {
269            consensus: LightningConfigConsensus {
270                threshold_pub_keys: PublicKeySet::from(Commitment::from(polynomial)),
271                fee_consensus: FeeConsensus::default(),
272                network: NetworkLegacyEncodingWrapper(params.consensus.network),
273            },
274            private: LightningConfigPrivate {
275                threshold_sec_key: SerdeSecret(SecretKeyShare::from_mut(&mut sks)),
276            },
277        };
278
279        Ok(server.to_erased())
280    }
281
282    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
283        let config = config.to_typed::<LightningConfig>()?;
284        if config.private.threshold_sec_key.public_key_share()
285            != config
286                .consensus
287                .threshold_pub_keys
288                .public_key_share(identity.to_usize())
289        {
290            bail!("Lightning private key doesn't match pubkey share");
291        }
292        Ok(())
293    }
294
295    fn get_client_config(
296        &self,
297        config: &ServerModuleConsensusConfig,
298    ) -> anyhow::Result<LightningClientConfig> {
299        let config = LightningConfigConsensus::from_erased(config)?;
300        Ok(LightningClientConfig {
301            threshold_pub_key: config.threshold_pub_keys.public_key(),
302            fee_consensus: config.fee_consensus,
303            network: config.network,
304        })
305    }
306
307    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
308        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
309    }
310}
311/// The lightning module implements an account system. It does not have the
312/// privacy guarantees of the e-cash mint module but instead allows for smart
313/// contracting. There exist two contract types that can be used to "lock"
314/// accounts:
315///
316///   * [Outgoing]: an account locked with an HTLC-like contract allowing to
317///     incentivize an external Lightning node to make payments for the funder
318///   * [Incoming]: a contract type that represents the acquisition of a
319///     preimage belonging to a hash. Every incoming contract is preceded by an
320///     offer that specifies how much the seller is asking for the preimage to a
321///     particular hash. It also contains some threshold-encrypted data. Once
322///     the contract is funded the data is decrypted. If it is a valid preimage
323///     the contract's funds are now accessible to the creator of the offer, if
324///     not they are accessible to the funder.
325///
326/// These two primitives allow to integrate the federation with the wider
327/// Lightning network through a centralized but untrusted (except for
328/// availability) Lightning gateway server.
329///
330/// [Outgoing]: fedimint_ln_common::contracts::outgoing::OutgoingContract
331/// [Incoming]: fedimint_ln_common::contracts::incoming::IncomingContract
332#[derive(Debug)]
333pub struct Lightning {
334    cfg: LightningConfig,
335    our_peer_id: PeerId,
336    server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
337}
338
339#[apply(async_trait_maybe_send!)]
340impl ServerModule for Lightning {
341    type Common = LightningModuleTypes;
342    type Init = LightningInit;
343
344    async fn consensus_proposal(
345        &self,
346        dbtx: &mut DatabaseTransaction<'_>,
347    ) -> Vec<LightningConsensusItem> {
348        let mut items: Vec<LightningConsensusItem> = dbtx
349            .find_by_prefix(&ProposeDecryptionShareKeyPrefix)
350            .await
351            .map(|(ProposeDecryptionShareKey(contract_id), share)| {
352                LightningConsensusItem::DecryptPreimage(contract_id, share)
353            })
354            .collect()
355            .await;
356
357        if let Ok(block_count_vote) = self.get_block_count() {
358            trace!(target: LOG_MODULE_LN, ?block_count_vote, "Proposing block count");
359            items.push(LightningConsensusItem::BlockCount(block_count_vote));
360        }
361
362        items
363    }
364
365    async fn process_consensus_item<'a, 'b>(
366        &'a self,
367        dbtx: &mut DatabaseTransaction<'b>,
368        consensus_item: LightningConsensusItem,
369        peer_id: PeerId,
370    ) -> anyhow::Result<()> {
371        let span = info_span!("process decryption share", %peer_id);
372        let _guard = span.enter();
373        trace!(target: LOG_MODULE_LN, ?consensus_item, "Processing consensus item proposal");
374
375        match consensus_item {
376            LightningConsensusItem::DecryptPreimage(contract_id, share) => {
377                if dbtx
378                    .get_value(&AgreedDecryptionShareKey(contract_id, peer_id))
379                    .await
380                    .is_some()
381                {
382                    bail!("Already received a valid decryption share for this peer");
383                }
384
385                let account = dbtx
386                    .get_value(&ContractKey(contract_id))
387                    .await
388                    .context("Contract account for this decryption share does not exist")?;
389
390                let (contract, out_point) = match account.contract {
391                    FundedContract::Incoming(contract) => (contract.contract, contract.out_point),
392                    FundedContract::Outgoing(..) => {
393                        bail!("Contract account for this decryption share is outgoing");
394                    }
395                };
396
397                if contract.decrypted_preimage != DecryptedPreimage::Pending {
398                    bail!("Contract for this decryption share is not pending");
399                }
400
401                if !self.validate_decryption_share(peer_id, &share, &contract.encrypted_preimage) {
402                    bail!("Decryption share is invalid");
403                }
404
405                // we save the first ordered valid decryption share for every peer
406                dbtx.insert_new_entry(&AgreedDecryptionShareKey(contract_id, peer_id), &share)
407                    .await;
408
409                // collect all valid decryption shares previously received for this contract
410                let decryption_shares = dbtx
411                    .find_by_prefix(&AgreedDecryptionShareContractIdPrefix(contract_id))
412                    .await
413                    .map(|(key, decryption_share)| (key.1, decryption_share))
414                    .collect::<Vec<_>>()
415                    .await;
416
417                if decryption_shares.len() < self.cfg.consensus.threshold() {
418                    return Ok(());
419                }
420
421                debug!(target: LOG_MODULE_LN, "Beginning to decrypt preimage");
422
423                let Ok(preimage_vec) = self.cfg.consensus.threshold_pub_keys.decrypt(
424                    decryption_shares
425                        .iter()
426                        .map(|(peer, share)| (peer.to_usize(), &share.0)),
427                    &contract.encrypted_preimage.0,
428                ) else {
429                    // TODO: check if that can happen even though shares are verified
430                    // before
431                    error!(target: LOG_MODULE_LN, contract_hash = %contract.hash, "Failed to decrypt preimage");
432                    return Ok(());
433                };
434
435                // Delete decryption shares once we've decrypted the preimage
436                dbtx.remove_entry(&ProposeDecryptionShareKey(contract_id))
437                    .await;
438
439                dbtx.remove_by_prefix(&AgreedDecryptionShareContractIdPrefix(contract_id))
440                    .await;
441
442                let decrypted_preimage = if preimage_vec.len() == 33
443                    && contract.hash
444                        == sha256::Hash::hash(&sha256::Hash::hash(&preimage_vec).to_byte_array())
445                {
446                    let preimage = PreimageKey(
447                        preimage_vec
448                            .as_slice()
449                            .try_into()
450                            .expect("Invalid preimage length"),
451                    );
452                    if preimage.to_public_key().is_ok() {
453                        DecryptedPreimage::Some(preimage)
454                    } else {
455                        DecryptedPreimage::Invalid
456                    }
457                } else {
458                    DecryptedPreimage::Invalid
459                };
460
461                debug!(target: LOG_MODULE_LN, ?decrypted_preimage);
462
463                // TODO: maybe define update helper fn
464                // Update contract
465                let contract_db_key = ContractKey(contract_id);
466                let mut contract_account = dbtx
467                    .get_value(&contract_db_key)
468                    .await
469                    .expect("checked before that it exists");
470                let incoming = match &mut contract_account.contract {
471                    FundedContract::Incoming(incoming) => incoming,
472                    FundedContract::Outgoing(_) => {
473                        unreachable!("previously checked that it's an incoming contract")
474                    }
475                };
476                incoming.contract.decrypted_preimage = decrypted_preimage.clone();
477                trace!(?contract_account, "Updating contract account");
478                dbtx.insert_entry(&contract_db_key, &contract_account).await;
479
480                // Update output outcome
481                let mut outcome = dbtx
482                    .get_value(&ContractUpdateKey(out_point))
483                    .await
484                    .expect("outcome was created on funding");
485
486                let LightningOutputOutcomeV0::Contract {
487                    outcome: ContractOutcome::Incoming(incoming_contract_outcome_preimage),
488                    ..
489                } = &mut outcome
490                else {
491                    panic!("We are expecting an incoming contract")
492                };
493                *incoming_contract_outcome_preimage = decrypted_preimage.clone();
494                dbtx.insert_entry(&ContractUpdateKey(out_point), &outcome)
495                    .await;
496            }
497            LightningConsensusItem::BlockCount(block_count) => {
498                let current_vote = dbtx
499                    .get_value(&BlockCountVoteKey(peer_id))
500                    .await
501                    .unwrap_or(0);
502
503                if block_count < current_vote {
504                    bail!("Block count vote decreased");
505                }
506
507                if block_count == current_vote {
508                    bail!("Block height vote is redundant");
509                }
510
511                dbtx.insert_entry(&BlockCountVoteKey(peer_id), &block_count)
512                    .await;
513            }
514            LightningConsensusItem::Default { variant, .. } => {
515                bail!("Unknown lightning consensus item received, variant={variant}");
516            }
517        }
518
519        Ok(())
520    }
521
522    async fn process_input<'a, 'b, 'c>(
523        &'a self,
524        dbtx: &mut DatabaseTransaction<'c>,
525        input: &'b LightningInput,
526        _in_point: InPoint,
527    ) -> Result<InputMeta, LightningInputError> {
528        let input = input.ensure_v0_ref()?;
529
530        let mut account = dbtx
531            .get_value(&ContractKey(input.contract_id))
532            .await
533            .ok_or(LightningInputError::UnknownContract(input.contract_id))?;
534
535        if account.amount < input.amount {
536            return Err(LightningInputError::InsufficientFunds(
537                account.amount,
538                input.amount,
539            ));
540        }
541
542        let consensus_block_count = self.consensus_block_count(dbtx).await;
543
544        let pub_key = match &account.contract {
545            FundedContract::Outgoing(outgoing) => {
546                if u64::from(outgoing.timelock) + 1 > consensus_block_count && !outgoing.cancelled {
547                    // If the timelock hasn't expired yet …
548                    let preimage_hash = bitcoin_hashes::sha256::Hash::hash(
549                        &input
550                            .witness
551                            .as_ref()
552                            .ok_or(LightningInputError::MissingPreimage)?
553                            .0,
554                    );
555
556                    // … and the spender provides a valid preimage …
557                    if preimage_hash != outgoing.hash {
558                        return Err(LightningInputError::InvalidPreimage);
559                    }
560
561                    // … then the contract account can be spent using the gateway key,
562                    outgoing.gateway_key
563                } else {
564                    // otherwise the user can claim the funds back.
565                    outgoing.user_key
566                }
567            }
568            FundedContract::Incoming(incoming) => match &incoming.contract.decrypted_preimage {
569                // Once the preimage has been decrypted …
570                DecryptedPreimage::Pending => {
571                    return Err(LightningInputError::ContractNotReady);
572                }
573                // … either the user may spend the funds since they sold a valid preimage …
574                DecryptedPreimage::Some(preimage) => match preimage.to_public_key() {
575                    Ok(pub_key) => pub_key,
576                    Err(_) => return Err(LightningInputError::InvalidPreimage),
577                },
578                // … or the gateway may claim back funds for not receiving the advertised preimage.
579                DecryptedPreimage::Invalid => incoming.contract.gateway_key,
580            },
581        };
582
583        account.amount -= input.amount;
584
585        dbtx.insert_entry(&ContractKey(input.contract_id), &account)
586            .await;
587
588        // When a contract reaches a terminal state, the associated amount will be
589        // updated to 0. At this point, the contract no longer needs to be tracked
590        // for auditing liabilities, so we can safely remove the audit key.
591        let audit_key = LightningAuditItemKey::from_funded_contract(&account.contract);
592        if account.amount.msats == 0 {
593            dbtx.remove_entry(&audit_key).await;
594        } else {
595            dbtx.insert_entry(&audit_key, &account.amount).await;
596        }
597
598        Ok(InputMeta {
599            amount: TransactionItemAmount {
600                amount: input.amount,
601                fee: self.cfg.consensus.fee_consensus.contract_input,
602            },
603            pub_key,
604        })
605    }
606
607    async fn process_output<'a, 'b>(
608        &'a self,
609        dbtx: &mut DatabaseTransaction<'b>,
610        output: &'a LightningOutput,
611        out_point: OutPoint,
612    ) -> Result<TransactionItemAmount, LightningOutputError> {
613        let output = output.ensure_v0_ref()?;
614
615        match output {
616            LightningOutputV0::Contract(contract) => {
617                // Incoming contracts are special, they need to match an offer
618                if let Contract::Incoming(incoming) = &contract.contract {
619                    let offer = dbtx
620                        .get_value(&OfferKey(incoming.hash))
621                        .await
622                        .ok_or(LightningOutputError::NoOffer(incoming.hash))?;
623
624                    if contract.amount < offer.amount {
625                        // If the account is not sufficiently funded fail the output
626                        return Err(LightningOutputError::InsufficientIncomingFunding(
627                            offer.amount,
628                            contract.amount,
629                        ));
630                    }
631                }
632
633                if contract.amount == Amount::ZERO {
634                    return Err(LightningOutputError::ZeroOutput);
635                }
636
637                let contract_db_key = ContractKey(contract.contract.contract_id());
638
639                let updated_contract_account = dbtx.get_value(&contract_db_key).await.map_or_else(
640                    || ContractAccount {
641                        amount: contract.amount,
642                        contract: contract.contract.clone().to_funded(out_point),
643                    },
644                    |mut value: ContractAccount| {
645                        value.amount += contract.amount;
646                        value
647                    },
648                );
649
650                dbtx.insert_entry(
651                    &LightningAuditItemKey::from_funded_contract(
652                        &updated_contract_account.contract,
653                    ),
654                    &updated_contract_account.amount,
655                )
656                .await;
657
658                if dbtx
659                    .insert_entry(&contract_db_key, &updated_contract_account)
660                    .await
661                    .is_none()
662                {
663                    dbtx.on_commit(move || {
664                        record_funded_contract_metric(&updated_contract_account);
665                    });
666                }
667
668                dbtx.insert_new_entry(
669                    &ContractUpdateKey(out_point),
670                    &LightningOutputOutcomeV0::Contract {
671                        id: contract.contract.contract_id(),
672                        outcome: contract.contract.to_outcome(),
673                    },
674                )
675                .await;
676
677                if let Contract::Incoming(incoming) = &contract.contract {
678                    let offer = dbtx
679                        .get_value(&OfferKey(incoming.hash))
680                        .await
681                        .expect("offer exists if output is valid");
682
683                    let decryption_share = self
684                        .cfg
685                        .private
686                        .threshold_sec_key
687                        .decrypt_share(&incoming.encrypted_preimage.0)
688                        .expect("We checked for decryption share validity on contract creation");
689
690                    dbtx.insert_new_entry(
691                        &ProposeDecryptionShareKey(contract.contract.contract_id()),
692                        &PreimageDecryptionShare(decryption_share),
693                    )
694                    .await;
695
696                    dbtx.remove_entry(&OfferKey(offer.hash)).await;
697                }
698
699                Ok(TransactionItemAmount {
700                    amount: contract.amount,
701                    fee: self.cfg.consensus.fee_consensus.contract_output,
702                })
703            }
704            LightningOutputV0::Offer(offer) => {
705                if !offer.encrypted_preimage.0.verify() {
706                    return Err(LightningOutputError::InvalidEncryptedPreimage);
707                }
708
709                // Check that each preimage is only offered for sale once, see #1397
710                if dbtx
711                    .insert_entry(
712                        &EncryptedPreimageIndexKey(offer.encrypted_preimage.consensus_hash()),
713                        &(),
714                    )
715                    .await
716                    .is_some()
717                {
718                    return Err(LightningOutputError::DuplicateEncryptedPreimage);
719                }
720
721                dbtx.insert_new_entry(
722                    &ContractUpdateKey(out_point),
723                    &LightningOutputOutcomeV0::Offer { id: offer.id() },
724                )
725                .await;
726
727                // TODO: sanity-check encrypted preimage size
728                if dbtx
729                    .insert_entry(&OfferKey(offer.hash), &(*offer).clone())
730                    .await
731                    .is_some()
732                {
733                    // Technically the error isn't due to a duplicate encrypted preimage but due to
734                    // a duplicate payment hash, practically it's the same problem though: re-using
735                    // the invoice key. Since we can't eaily extend the error enum we just re-use
736                    // this variant.
737                    return Err(LightningOutputError::DuplicateEncryptedPreimage);
738                }
739
740                dbtx.on_commit(|| {
741                    LN_INCOMING_OFFER.inc();
742                });
743
744                Ok(TransactionItemAmount::ZERO)
745            }
746            LightningOutputV0::CancelOutgoing {
747                contract,
748                gateway_signature,
749            } => {
750                let contract_account = dbtx
751                    .get_value(&ContractKey(*contract))
752                    .await
753                    .ok_or(LightningOutputError::UnknownContract(*contract))?;
754
755                let outgoing_contract = match &contract_account.contract {
756                    FundedContract::Outgoing(contract) => contract,
757                    FundedContract::Incoming(_) => {
758                        return Err(LightningOutputError::NotOutgoingContract);
759                    }
760                };
761
762                SECP256K1
763                    .verify_schnorr(
764                        gateway_signature,
765                        &Message::from_digest(*outgoing_contract.cancellation_message().as_ref()),
766                        &outgoing_contract.gateway_key.x_only_public_key().0,
767                    )
768                    .map_err(|_| LightningOutputError::InvalidCancellationSignature)?;
769
770                let updated_contract_account = {
771                    let mut contract_account = dbtx
772                        .get_value(&ContractKey(*contract))
773                        .await
774                        .expect("Contract exists if output is valid");
775
776                    let outgoing_contract = match &mut contract_account.contract {
777                        FundedContract::Outgoing(contract) => contract,
778                        FundedContract::Incoming(_) => {
779                            panic!("Contract type was checked in validate_output");
780                        }
781                    };
782
783                    outgoing_contract.cancelled = true;
784
785                    contract_account
786                };
787
788                dbtx.insert_entry(&ContractKey(*contract), &updated_contract_account)
789                    .await;
790
791                dbtx.insert_new_entry(
792                    &ContractUpdateKey(out_point),
793                    &LightningOutputOutcomeV0::CancelOutgoingContract { id: *contract },
794                )
795                .await;
796
797                dbtx.on_commit(|| {
798                    LN_CANCEL_OUTGOING_CONTRACTS.inc();
799                });
800
801                Ok(TransactionItemAmount::ZERO)
802            }
803        }
804    }
805
806    async fn output_status(
807        &self,
808        dbtx: &mut DatabaseTransaction<'_>,
809        out_point: OutPoint,
810    ) -> Option<LightningOutputOutcome> {
811        dbtx.get_value(&ContractUpdateKey(out_point))
812            .await
813            .map(LightningOutputOutcome::V0)
814    }
815
816    async fn audit(
817        &self,
818        dbtx: &mut DatabaseTransaction<'_>,
819        audit: &mut Audit,
820        module_instance_id: ModuleInstanceId,
821    ) {
822        audit
823            .add_items(
824                dbtx,
825                module_instance_id,
826                &LightningAuditItemKeyPrefix,
827                // Both incoming and outgoing contracts represent liabilities to the federation
828                // since they are obligations to issue notes.
829                |_, v| -(v.msats as i64),
830            )
831            .await;
832    }
833
834    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
835        vec![
836            api_endpoint! {
837                BLOCK_COUNT_ENDPOINT,
838                ApiVersion::new(0, 0),
839                async |module: &Lightning, context, _v: ()| -> Option<u64> {
840                    Ok(Some(module.consensus_block_count(&mut context.dbtx().into_nc()).await))
841                }
842            },
843            api_endpoint! {
844                ACCOUNT_ENDPOINT,
845                ApiVersion::new(0, 0),
846                async |module: &Lightning, context, contract_id: ContractId| -> Option<ContractAccount> {
847                    Ok(module
848                        .get_contract_account(&mut context.dbtx().into_nc(), contract_id)
849                        .await)
850                }
851            },
852            api_endpoint! {
853                AWAIT_ACCOUNT_ENDPOINT,
854                ApiVersion::new(0, 0),
855                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
856                    Ok(module
857                        .wait_contract_account(context, contract_id)
858                        .await)
859                }
860            },
861            api_endpoint! {
862                AWAIT_BLOCK_HEIGHT_ENDPOINT,
863                ApiVersion::new(0, 0),
864                async |module: &Lightning, context, block_height: u64| -> () {
865                    module.wait_block_height(block_height, &mut context.dbtx().into_nc()).await;
866                    Ok(())
867                }
868            },
869            api_endpoint! {
870                AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT,
871                ApiVersion::new(0, 0),
872                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
873                    Ok(module.wait_outgoing_contract_account_cancelled(context, contract_id).await)
874                }
875            },
876            api_endpoint! {
877                GET_DECRYPTED_PREIMAGE_STATUS,
878                ApiVersion::new(0, 0),
879                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, DecryptedPreimageStatus) {
880                    Ok(module.get_decrypted_preimage_status(context, contract_id).await)
881                }
882            },
883            api_endpoint! {
884                AWAIT_PREIMAGE_DECRYPTION,
885                ApiVersion::new(0, 0),
886                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, Option<Preimage>) {
887                    Ok(module.wait_preimage_decrypted(context, contract_id).await)
888                }
889            },
890            api_endpoint! {
891                OFFER_ENDPOINT,
892                ApiVersion::new(0, 0),
893                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> Option<IncomingContractOffer> {
894                    Ok(module
895                        .get_offer(&mut context.dbtx().into_nc(), payment_hash)
896                        .await)
897               }
898            },
899            api_endpoint! {
900                AWAIT_OFFER_ENDPOINT,
901                ApiVersion::new(0, 0),
902                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> IncomingContractOffer {
903                    Ok(module
904                        .wait_offer(context, payment_hash)
905                        .await)
906                }
907            },
908            api_endpoint! {
909                LIST_GATEWAYS_ENDPOINT,
910                ApiVersion::new(0, 0),
911                async |module: &Lightning, context, _v: ()| -> Vec<LightningGatewayAnnouncement> {
912                    Ok(module.list_gateways(&mut context.dbtx().into_nc()).await)
913                }
914            },
915            api_endpoint! {
916                REGISTER_GATEWAY_ENDPOINT,
917                ApiVersion::new(0, 0),
918                async |module: &Lightning, context, gateway: LightningGatewayAnnouncement| -> () {
919                    module.register_gateway(&mut context.dbtx().into_nc(), gateway).await;
920                    Ok(())
921                }
922            },
923            api_endpoint! {
924                REMOVE_GATEWAY_CHALLENGE_ENDPOINT,
925                ApiVersion::new(0, 1),
926                async |module: &Lightning, context, gateway_id: PublicKey| -> Option<sha256::Hash> {
927                    Ok(module.get_gateway_remove_challenge(gateway_id, &mut context.dbtx().into_nc()).await)
928                }
929            },
930            api_endpoint! {
931                REMOVE_GATEWAY_ENDPOINT,
932                ApiVersion::new(0, 1),
933                async |module: &Lightning, context, remove_gateway_request: RemoveGatewayRequest| -> bool {
934                    match module.remove_gateway(remove_gateway_request.clone(), &mut context.dbtx().into_nc()).await {
935                        Ok(()) => Ok(true),
936                        Err(e) => {
937                            warn!(target: LOG_MODULE_LN, "Unable to remove gateway registration {remove_gateway_request:?} Error: {e:?}");
938                            Ok(false)
939                        },
940                    }
941                }
942            },
943        ]
944    }
945}
946
947impl Lightning {
948    fn get_block_count(&self) -> anyhow::Result<u64> {
949        self.server_bitcoin_rpc_monitor
950            .status()
951            .map(|status| status.block_count)
952            .context("Block count not available yet")
953    }
954
955    async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
956        let peer_count = 3 * (self.cfg.consensus.threshold() / 2) + 1;
957
958        let mut counts = dbtx
959            .find_by_prefix(&BlockCountVotePrefix)
960            .await
961            .map(|(.., count)| count)
962            .collect::<Vec<_>>()
963            .await;
964
965        assert!(counts.len() <= peer_count);
966
967        while counts.len() < peer_count {
968            counts.push(0);
969        }
970
971        counts.sort_unstable();
972
973        counts[peer_count / 2]
974    }
975
976    async fn wait_block_height(&self, block_height: u64, dbtx: &mut DatabaseTransaction<'_>) {
977        while block_height >= self.consensus_block_count(dbtx).await {
978            sleep(Duration::from_secs(5)).await;
979        }
980    }
981
982    fn validate_decryption_share(
983        &self,
984        peer: PeerId,
985        share: &PreimageDecryptionShare,
986        message: &EncryptedPreimage,
987    ) -> bool {
988        self.cfg
989            .consensus
990            .threshold_pub_keys
991            .public_key_share(peer.to_usize())
992            .verify_decryption_share(&share.0, &message.0)
993    }
994
995    async fn get_offer(
996        &self,
997        dbtx: &mut DatabaseTransaction<'_>,
998        payment_hash: bitcoin_hashes::sha256::Hash,
999    ) -> Option<IncomingContractOffer> {
1000        dbtx.get_value(&OfferKey(payment_hash)).await
1001    }
1002
1003    async fn wait_offer(
1004        &self,
1005        context: &mut ApiEndpointContext<'_>,
1006        payment_hash: bitcoin_hashes::sha256::Hash,
1007    ) -> IncomingContractOffer {
1008        let future = context.wait_key_exists(OfferKey(payment_hash));
1009        future.await
1010    }
1011
1012    async fn get_contract_account(
1013        &self,
1014        dbtx: &mut DatabaseTransaction<'_>,
1015        contract_id: ContractId,
1016    ) -> Option<ContractAccount> {
1017        dbtx.get_value(&ContractKey(contract_id)).await
1018    }
1019
1020    async fn wait_contract_account(
1021        &self,
1022        context: &mut ApiEndpointContext<'_>,
1023        contract_id: ContractId,
1024    ) -> ContractAccount {
1025        // not using a variable here leads to a !Send error
1026        let future = context.wait_key_exists(ContractKey(contract_id));
1027        future.await
1028    }
1029
1030    async fn wait_outgoing_contract_account_cancelled(
1031        &self,
1032        context: &mut ApiEndpointContext<'_>,
1033        contract_id: ContractId,
1034    ) -> ContractAccount {
1035        let future =
1036            context.wait_value_matches(ContractKey(contract_id), |contract| {
1037                match &contract.contract {
1038                    FundedContract::Outgoing(c) => c.cancelled,
1039                    FundedContract::Incoming(_) => false,
1040                }
1041            });
1042        future.await
1043    }
1044
1045    async fn get_decrypted_preimage_status(
1046        &self,
1047        context: &mut ApiEndpointContext<'_>,
1048        contract_id: ContractId,
1049    ) -> (IncomingContractAccount, DecryptedPreimageStatus) {
1050        let f_contract = context.wait_key_exists(ContractKey(contract_id));
1051        let contract = f_contract.await;
1052        let incoming_contract_account = Self::get_incoming_contract_account(contract);
1053        match &incoming_contract_account.contract.decrypted_preimage {
1054            DecryptedPreimage::Some(key) => (
1055                incoming_contract_account.clone(),
1056                DecryptedPreimageStatus::Some(Preimage(sha256::Hash::hash(&key.0).to_byte_array())),
1057            ),
1058            DecryptedPreimage::Pending => {
1059                (incoming_contract_account, DecryptedPreimageStatus::Pending)
1060            }
1061            DecryptedPreimage::Invalid => {
1062                (incoming_contract_account, DecryptedPreimageStatus::Invalid)
1063            }
1064        }
1065    }
1066
1067    async fn wait_preimage_decrypted(
1068        &self,
1069        context: &mut ApiEndpointContext<'_>,
1070        contract_id: ContractId,
1071    ) -> (IncomingContractAccount, Option<Preimage>) {
1072        let future =
1073            context.wait_value_matches(ContractKey(contract_id), |contract| {
1074                match &contract.contract {
1075                    FundedContract::Incoming(c) => match c.contract.decrypted_preimage {
1076                        DecryptedPreimage::Pending => false,
1077                        DecryptedPreimage::Some(_) | DecryptedPreimage::Invalid => true,
1078                    },
1079                    FundedContract::Outgoing(_) => false,
1080                }
1081            });
1082
1083        let decrypt_preimage = future.await;
1084        let incoming_contract_account = Self::get_incoming_contract_account(decrypt_preimage);
1085        match incoming_contract_account
1086            .clone()
1087            .contract
1088            .decrypted_preimage
1089        {
1090            DecryptedPreimage::Some(key) => (
1091                incoming_contract_account,
1092                Some(Preimage(sha256::Hash::hash(&key.0).to_byte_array())),
1093            ),
1094            _ => (incoming_contract_account, None),
1095        }
1096    }
1097
1098    fn get_incoming_contract_account(contract: ContractAccount) -> IncomingContractAccount {
1099        if let FundedContract::Incoming(incoming) = contract.contract {
1100            return IncomingContractAccount {
1101                amount: contract.amount,
1102                contract: incoming.contract,
1103            };
1104        }
1105
1106        panic!("Contract is not an IncomingContractAccount");
1107    }
1108
1109    async fn list_gateways(
1110        &self,
1111        dbtx: &mut DatabaseTransaction<'_>,
1112    ) -> Vec<LightningGatewayAnnouncement> {
1113        let stream = dbtx.find_by_prefix(&LightningGatewayKeyPrefix).await;
1114        stream
1115            .filter_map(|(_, gw)| async { if gw.is_expired() { None } else { Some(gw) } })
1116            .collect::<Vec<LightningGatewayRegistration>>()
1117            .await
1118            .into_iter()
1119            .map(LightningGatewayRegistration::unanchor)
1120            .collect::<Vec<LightningGatewayAnnouncement>>()
1121    }
1122
1123    async fn register_gateway(
1124        &self,
1125        dbtx: &mut DatabaseTransaction<'_>,
1126        gateway: LightningGatewayAnnouncement,
1127    ) {
1128        // Garbage collect expired gateways (since we're already writing to the DB)
1129        // Note: A "gotcha" of doing this here is that if two gateways are registered
1130        // at the same time, they will both attempt to delete the same expired gateways
1131        // and one of them will fail. This should be fine, since the other one will
1132        // succeed and the failed one will just try again.
1133        self.delete_expired_gateways(dbtx).await;
1134
1135        dbtx.insert_entry(
1136            &LightningGatewayKey(gateway.info.gateway_id),
1137            &gateway.anchor(),
1138        )
1139        .await;
1140    }
1141
1142    async fn delete_expired_gateways(&self, dbtx: &mut DatabaseTransaction<'_>) {
1143        let expired_gateway_keys = dbtx
1144            .find_by_prefix(&LightningGatewayKeyPrefix)
1145            .await
1146            .filter_map(|(key, gw)| async move { if gw.is_expired() { Some(key) } else { None } })
1147            .collect::<Vec<LightningGatewayKey>>()
1148            .await;
1149
1150        for key in expired_gateway_keys {
1151            dbtx.remove_entry(&key).await;
1152        }
1153    }
1154
1155    /// Returns the challenge to the gateway that must be signed by the
1156    /// gateway's private key in order for the gateway registration record
1157    /// to be removed. The challenge is the concatenation of the gateway's
1158    /// public key and the `valid_until` bytes. This ensures that the
1159    /// challenges changes every time the gateway is re-registered and ensures
1160    /// that the challenge is unique per-gateway.
1161    async fn get_gateway_remove_challenge(
1162        &self,
1163        gateway_id: PublicKey,
1164        dbtx: &mut DatabaseTransaction<'_>,
1165    ) -> Option<sha256::Hash> {
1166        match dbtx.get_value(&LightningGatewayKey(gateway_id)).await {
1167            Some(gateway) => {
1168                let mut valid_until_bytes = gateway.valid_until.to_bytes();
1169                let mut challenge_bytes = gateway_id.to_bytes();
1170                challenge_bytes.append(&mut valid_until_bytes);
1171                Some(sha256::Hash::hash(&challenge_bytes))
1172            }
1173            _ => None,
1174        }
1175    }
1176
1177    /// Removes the gateway registration record. First the signature provided by
1178    /// the gateway is verified by checking if the gateway's challenge has
1179    /// been signed by the gateway's private key.
1180    async fn remove_gateway(
1181        &self,
1182        remove_gateway_request: RemoveGatewayRequest,
1183        dbtx: &mut DatabaseTransaction<'_>,
1184    ) -> anyhow::Result<()> {
1185        let fed_public_key = self.cfg.consensus.threshold_pub_keys.public_key();
1186        let gateway_id = remove_gateway_request.gateway_id;
1187        let our_peer_id = self.our_peer_id;
1188        let signature = remove_gateway_request
1189            .signatures
1190            .get(&our_peer_id)
1191            .ok_or_else(|| {
1192                warn!(target: LOG_MODULE_LN, "No signature provided for gateway: {gateway_id}");
1193                anyhow::anyhow!("No signature provided for gateway {gateway_id}")
1194            })?;
1195
1196        // If there is no challenge, the gateway does not exist in the database and
1197        // there is nothing to do
1198        let challenge = self
1199            .get_gateway_remove_challenge(gateway_id, dbtx)
1200            .await
1201            .ok_or(anyhow::anyhow!(
1202                "Gateway {gateway_id} is not registered with peer {our_peer_id}"
1203            ))?;
1204
1205        // Verify the supplied schnorr signature is valid
1206        let msg = create_gateway_remove_message(fed_public_key, our_peer_id, challenge);
1207        signature.verify(&msg, &gateway_id.x_only_public_key().0)?;
1208
1209        dbtx.remove_entry(&LightningGatewayKey(gateway_id)).await;
1210        info!(target: LOG_MODULE_LN, "Successfully removed gateway: {gateway_id}");
1211        Ok(())
1212    }
1213}
1214
1215fn record_funded_contract_metric(updated_contract_account: &ContractAccount) {
1216    LN_FUNDED_CONTRACT_SATS
1217        .with_label_values(&[match updated_contract_account.contract {
1218            FundedContract::Incoming(_) => "incoming",
1219            FundedContract::Outgoing(_) => "outgoing",
1220        }])
1221        .observe(updated_contract_account.amount.sats_f64());
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226    use std::time::Duration;
1227
1228    use assert_matches::assert_matches;
1229    use bitcoin_hashes::{Hash as BitcoinHash, sha256};
1230    use fedimint_core::bitcoin::{Block, BlockHash};
1231    use fedimint_core::config::ConfigGenModuleParams;
1232    use fedimint_core::db::mem_impl::MemDatabase;
1233    use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
1234    use fedimint_core::encoding::Encodable;
1235    use fedimint_core::envs::BitcoinRpcConfig;
1236    use fedimint_core::module::registry::ModuleRegistry;
1237    use fedimint_core::module::{InputMeta, TransactionItemAmount};
1238    use fedimint_core::secp256k1::{PublicKey, generate_keypair};
1239    use fedimint_core::task::TaskGroup;
1240    use fedimint_core::util::SafeUrl;
1241    use fedimint_core::{Amount, Feerate, InPoint, OutPoint, PeerId, TransactionId};
1242    use fedimint_ln_common::config::{
1243        LightningClientConfig, LightningConfig, LightningGenParams, LightningGenParamsConsensus,
1244        LightningGenParamsLocal, Network,
1245    };
1246    use fedimint_ln_common::contracts::incoming::{
1247        FundedIncomingContract, IncomingContract, IncomingContractOffer,
1248    };
1249    use fedimint_ln_common::contracts::outgoing::OutgoingContract;
1250    use fedimint_ln_common::contracts::{
1251        DecryptedPreimage, EncryptedPreimage, FundedContract, IdentifiableContract, Preimage,
1252        PreimageKey,
1253    };
1254    use fedimint_ln_common::{ContractAccount, LightningInput, LightningOutput};
1255    use fedimint_server_core::bitcoin_rpc::{IServerBitcoinRpc, ServerBitcoinRpcMonitor};
1256    use fedimint_server_core::{ServerModule, ServerModuleInit};
1257    use rand::rngs::OsRng;
1258
1259    use crate::db::{ContractKey, LightningAuditItemKey};
1260    use crate::{Lightning, LightningInit};
1261
1262    #[derive(Debug)]
1263    struct MockBitcoinServerRpc;
1264
1265    #[async_trait::async_trait]
1266    impl IServerBitcoinRpc for MockBitcoinServerRpc {
1267        fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
1268            BitcoinRpcConfig {
1269                kind: "mock".to_string(),
1270                url: "http://mock".parse().unwrap(),
1271            }
1272        }
1273
1274        fn get_url(&self) -> SafeUrl {
1275            "http://mock".parse().unwrap()
1276        }
1277
1278        async fn get_network(&self) -> anyhow::Result<Network> {
1279            Err(anyhow::anyhow!("Mock network error"))
1280        }
1281
1282        async fn get_block_count(&self) -> anyhow::Result<u64> {
1283            Err(anyhow::anyhow!("Mock block count error"))
1284        }
1285
1286        async fn get_block_hash(&self, _height: u64) -> anyhow::Result<BlockHash> {
1287            Err(anyhow::anyhow!("Mock block hash error"))
1288        }
1289
1290        async fn get_block(&self, _block_hash: &BlockHash) -> anyhow::Result<Block> {
1291            Err(anyhow::anyhow!("Mock block error"))
1292        }
1293
1294        async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
1295            Err(anyhow::anyhow!("Mock feerate error"))
1296        }
1297
1298        async fn submit_transaction(&self, _transaction: fedimint_core::bitcoin::Transaction) {
1299            // No-op for mock
1300        }
1301
1302        async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
1303            Err(anyhow::anyhow!("Mock sync percentage error"))
1304        }
1305    }
1306
1307    const MINTS: u16 = 4;
1308
1309    fn build_configs() -> (Vec<LightningConfig>, LightningClientConfig) {
1310        let peers = (0..MINTS).map(PeerId::from).collect::<Vec<_>>();
1311        let server_cfg = ServerModuleInit::trusted_dealer_gen(
1312            &LightningInit,
1313            &peers,
1314            &ConfigGenModuleParams::from_typed(LightningGenParams {
1315                local: LightningGenParamsLocal {
1316                    bitcoin_rpc: BitcoinRpcConfig {
1317                        kind: "bitcoind".to_string(),
1318                        url: "http://localhost:18332".parse().unwrap(),
1319                    },
1320                },
1321                consensus: LightningGenParamsConsensus {
1322                    network: Network::Regtest,
1323                },
1324            })
1325            .expect("valid config params"),
1326        );
1327
1328        let client_cfg = ServerModuleInit::get_client_config(
1329            &LightningInit,
1330            &server_cfg[&PeerId::from(0)].consensus,
1331        )
1332        .unwrap();
1333
1334        let server_cfg = server_cfg
1335            .into_values()
1336            .map(|config| {
1337                config
1338                    .to_typed()
1339                    .expect("Config was just generated by the same configgen")
1340            })
1341            .collect::<Vec<LightningConfig>>();
1342
1343        (server_cfg, client_cfg)
1344    }
1345
1346    fn random_pub_key() -> PublicKey {
1347        generate_keypair(&mut OsRng).1
1348    }
1349
1350    #[test_log::test(tokio::test)]
1351    async fn encrypted_preimage_only_usable_once() {
1352        let task_group = TaskGroup::new();
1353        let (server_cfg, client_cfg) = build_configs();
1354
1355        let server = Lightning {
1356            cfg: server_cfg[0].clone(),
1357            our_peer_id: 0.into(),
1358            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1359                MockBitcoinServerRpc.into_dyn(),
1360                Duration::from_secs(1),
1361                &task_group,
1362            ),
1363        };
1364
1365        let preimage = [42u8; 32];
1366        let encrypted_preimage = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([42; 32]));
1367
1368        let hash = preimage.consensus_hash();
1369        let offer = IncomingContractOffer {
1370            amount: Amount::from_sats(10),
1371            hash,
1372            encrypted_preimage: encrypted_preimage.clone(),
1373            expiry_time: None,
1374        };
1375        let output = LightningOutput::new_v0_offer(offer);
1376        let out_point = OutPoint {
1377            txid: TransactionId::all_zeros(),
1378            out_idx: 0,
1379        };
1380
1381        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1382        let mut dbtx = db.begin_transaction_nc().await;
1383
1384        server
1385            .process_output(
1386                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1387                &output,
1388                out_point,
1389            )
1390            .await
1391            .expect("First time works");
1392
1393        let hash2 = [21u8, 32].consensus_hash();
1394        let offer2 = IncomingContractOffer {
1395            amount: Amount::from_sats(1),
1396            hash: hash2,
1397            encrypted_preimage,
1398            expiry_time: None,
1399        };
1400        let output2 = LightningOutput::new_v0_offer(offer2);
1401        let out_point2 = OutPoint {
1402            txid: TransactionId::all_zeros(),
1403            out_idx: 1,
1404        };
1405
1406        assert_matches!(
1407            server
1408                .process_output(
1409                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1410                    &output2,
1411                    out_point2
1412                )
1413                .await,
1414            Err(_)
1415        );
1416    }
1417
1418    #[test_log::test(tokio::test)]
1419    async fn process_input_for_valid_incoming_contracts() {
1420        let task_group = TaskGroup::new();
1421        let (server_cfg, client_cfg) = build_configs();
1422        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1423        let mut dbtx = db.begin_transaction_nc().await;
1424        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
1425
1426        let server = Lightning {
1427            cfg: server_cfg[0].clone(),
1428            our_peer_id: 0.into(),
1429            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1430                MockBitcoinServerRpc.into_dyn(),
1431                Duration::from_secs(1),
1432                &task_group,
1433            ),
1434        };
1435
1436        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
1437        let funded_incoming_contract = FundedContract::Incoming(FundedIncomingContract {
1438            contract: IncomingContract {
1439                hash: sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array()),
1440                encrypted_preimage: EncryptedPreimage(
1441                    client_cfg.threshold_pub_key.encrypt(preimage.0),
1442                ),
1443                decrypted_preimage: DecryptedPreimage::Some(preimage.clone()),
1444                gateway_key: random_pub_key(),
1445            },
1446            out_point: OutPoint {
1447                txid: TransactionId::all_zeros(),
1448                out_idx: 0,
1449            },
1450        });
1451
1452        let contract_id = funded_incoming_contract.contract_id();
1453        let audit_key = LightningAuditItemKey::from_funded_contract(&funded_incoming_contract);
1454        let amount = Amount { msats: 1000 };
1455        let lightning_input = LightningInput::new_v0(contract_id, amount, None);
1456
1457        module_dbtx.insert_new_entry(&audit_key, &amount).await;
1458        module_dbtx
1459            .insert_new_entry(
1460                &ContractKey(contract_id),
1461                &ContractAccount {
1462                    amount,
1463                    contract: funded_incoming_contract,
1464                },
1465            )
1466            .await;
1467
1468        let processed_input_meta = server
1469            .process_input(
1470                &mut module_dbtx.to_ref_nc(),
1471                &lightning_input,
1472                InPoint {
1473                    txid: TransactionId::all_zeros(),
1474                    in_idx: 0,
1475                },
1476            )
1477            .await
1478            .expect("should process valid incoming contract");
1479        let expected_input_meta = InputMeta {
1480            amount: TransactionItemAmount {
1481                amount,
1482                fee: Amount { msats: 0 },
1483            },
1484            pub_key: preimage
1485                .to_public_key()
1486                .expect("should create Schnorr pubkey from preimage"),
1487        };
1488
1489        assert_eq!(processed_input_meta, expected_input_meta);
1490
1491        let audit_item = module_dbtx.get_value(&audit_key).await;
1492        assert_eq!(audit_item, None);
1493    }
1494
1495    #[test_log::test(tokio::test)]
1496    async fn process_input_for_valid_outgoing_contracts() {
1497        let task_group = TaskGroup::new();
1498        let (server_cfg, _) = build_configs();
1499        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1500        let mut dbtx = db.begin_transaction_nc().await;
1501        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
1502
1503        let server = Lightning {
1504            cfg: server_cfg[0].clone(),
1505            our_peer_id: 0.into(),
1506            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1507                MockBitcoinServerRpc.into_dyn(),
1508                Duration::from_secs(1),
1509                &task_group,
1510            ),
1511        };
1512
1513        let preimage = Preimage([42u8; 32]);
1514        let gateway_key = random_pub_key();
1515        let outgoing_contract = FundedContract::Outgoing(OutgoingContract {
1516            hash: preimage.consensus_hash(),
1517            gateway_key,
1518            timelock: 1_000_000,
1519            user_key: random_pub_key(),
1520            cancelled: false,
1521        });
1522        let contract_id = outgoing_contract.contract_id();
1523        let audit_key = LightningAuditItemKey::from_funded_contract(&outgoing_contract);
1524        let amount = Amount { msats: 1000 };
1525        let lightning_input = LightningInput::new_v0(contract_id, amount, Some(preimage.clone()));
1526
1527        module_dbtx.insert_new_entry(&audit_key, &amount).await;
1528        module_dbtx
1529            .insert_new_entry(
1530                &ContractKey(contract_id),
1531                &ContractAccount {
1532                    amount,
1533                    contract: outgoing_contract,
1534                },
1535            )
1536            .await;
1537
1538        let processed_input_meta = server
1539            .process_input(
1540                &mut module_dbtx.to_ref_nc(),
1541                &lightning_input,
1542                InPoint {
1543                    txid: TransactionId::all_zeros(),
1544                    in_idx: 0,
1545                },
1546            )
1547            .await
1548            .expect("should process valid outgoing contract");
1549
1550        let expected_input_meta = InputMeta {
1551            amount: TransactionItemAmount {
1552                amount,
1553                fee: Amount { msats: 0 },
1554            },
1555            pub_key: gateway_key,
1556        };
1557
1558        assert_eq!(processed_input_meta, expected_input_meta);
1559
1560        let audit_item = module_dbtx.get_value(&audit_key).await;
1561        assert_eq!(audit_item, None);
1562    }
1563}