fedimint_ln_server/
lib.rs

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