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