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