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