Skip to main content

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