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                    Ok(Some(module.consensus_block_count(&mut context.dbtx().into_nc()).await))
840                }
841            },
842            api_endpoint! {
843                ACCOUNT_ENDPOINT,
844                ApiVersion::new(0, 0),
845                async |module: &Lightning, context, contract_id: ContractId| -> Option<ContractAccount> {
846                    Ok(module
847                        .get_contract_account(&mut context.dbtx().into_nc(), contract_id)
848                        .await)
849                }
850            },
851            api_endpoint! {
852                AWAIT_ACCOUNT_ENDPOINT,
853                ApiVersion::new(0, 0),
854                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
855                    Ok(module
856                        .wait_contract_account(context, contract_id)
857                        .await)
858                }
859            },
860            api_endpoint! {
861                AWAIT_BLOCK_HEIGHT_ENDPOINT,
862                ApiVersion::new(0, 0),
863                async |module: &Lightning, context, block_height: u64| -> () {
864                    module.wait_block_height(block_height, &mut context.dbtx().into_nc()).await;
865                    Ok(())
866                }
867            },
868            api_endpoint! {
869                AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT,
870                ApiVersion::new(0, 0),
871                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
872                    Ok(module.wait_outgoing_contract_account_cancelled(context, contract_id).await)
873                }
874            },
875            api_endpoint! {
876                GET_DECRYPTED_PREIMAGE_STATUS,
877                ApiVersion::new(0, 0),
878                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, DecryptedPreimageStatus) {
879                    Ok(module.get_decrypted_preimage_status(context, contract_id).await)
880                }
881            },
882            api_endpoint! {
883                AWAIT_PREIMAGE_DECRYPTION,
884                ApiVersion::new(0, 0),
885                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, Option<Preimage>) {
886                    Ok(module.wait_preimage_decrypted(context, contract_id).await)
887                }
888            },
889            api_endpoint! {
890                OFFER_ENDPOINT,
891                ApiVersion::new(0, 0),
892                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> Option<IncomingContractOffer> {
893                    Ok(module
894                        .get_offer(&mut context.dbtx().into_nc(), payment_hash)
895                        .await)
896               }
897            },
898            api_endpoint! {
899                AWAIT_OFFER_ENDPOINT,
900                ApiVersion::new(0, 0),
901                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> IncomingContractOffer {
902                    Ok(module
903                        .wait_offer(context, payment_hash)
904                        .await)
905                }
906            },
907            api_endpoint! {
908                LIST_GATEWAYS_ENDPOINT,
909                ApiVersion::new(0, 0),
910                async |module: &Lightning, context, _v: ()| -> Vec<LightningGatewayAnnouncement> {
911                    Ok(module.list_gateways(&mut context.dbtx().into_nc()).await)
912                }
913            },
914            api_endpoint! {
915                REGISTER_GATEWAY_ENDPOINT,
916                ApiVersion::new(0, 0),
917                async |module: &Lightning, context, gateway: LightningGatewayAnnouncement| -> () {
918                    module.register_gateway(&mut context.dbtx().into_nc(), gateway).await;
919                    Ok(())
920                }
921            },
922            api_endpoint! {
923                REMOVE_GATEWAY_CHALLENGE_ENDPOINT,
924                ApiVersion::new(0, 1),
925                async |module: &Lightning, context, gateway_id: PublicKey| -> Option<sha256::Hash> {
926                    Ok(module.get_gateway_remove_challenge(gateway_id, &mut context.dbtx().into_nc()).await)
927                }
928            },
929            api_endpoint! {
930                REMOVE_GATEWAY_ENDPOINT,
931                ApiVersion::new(0, 1),
932                async |module: &Lightning, context, remove_gateway_request: RemoveGatewayRequest| -> bool {
933                    match module.remove_gateway(remove_gateway_request.clone(), &mut context.dbtx().into_nc()).await {
934                        Ok(()) => Ok(true),
935                        Err(err) => {
936                            warn!(target: LOG_MODULE_LN, err = %err.fmt_compact_anyhow(), gateway_id = %remove_gateway_request.gateway_id, "Unable to remove gateway registration");
937                            Ok(false)
938                        },
939                    }
940                }
941            },
942        ]
943    }
944}
945
946impl Lightning {
947    fn get_block_count(&self) -> anyhow::Result<u64> {
948        self.server_bitcoin_rpc_monitor
949            .status()
950            .map(|status| status.block_count)
951            .context("Block count not available yet")
952    }
953
954    async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
955        let peer_count = 3 * (self.cfg.consensus.threshold() / 2) + 1;
956
957        let mut counts = dbtx
958            .find_by_prefix(&BlockCountVotePrefix)
959            .await
960            .map(|(.., count)| count)
961            .collect::<Vec<_>>()
962            .await;
963
964        assert!(counts.len() <= peer_count);
965
966        while counts.len() < peer_count {
967            counts.push(0);
968        }
969
970        counts.sort_unstable();
971
972        counts[peer_count / 2]
973    }
974
975    async fn wait_block_height(&self, block_height: u64, dbtx: &mut DatabaseTransaction<'_>) {
976        while block_height >= self.consensus_block_count(dbtx).await {
977            sleep(Duration::from_secs(5)).await;
978        }
979    }
980
981    fn validate_decryption_share(
982        &self,
983        peer: PeerId,
984        share: &PreimageDecryptionShare,
985        message: &EncryptedPreimage,
986    ) -> bool {
987        self.cfg
988            .consensus
989            .threshold_pub_keys
990            .public_key_share(peer.to_usize())
991            .verify_decryption_share(&share.0, &message.0)
992    }
993
994    async fn get_offer(
995        &self,
996        dbtx: &mut DatabaseTransaction<'_>,
997        payment_hash: bitcoin_hashes::sha256::Hash,
998    ) -> Option<IncomingContractOffer> {
999        dbtx.get_value(&OfferKey(payment_hash)).await
1000    }
1001
1002    async fn wait_offer(
1003        &self,
1004        context: &mut ApiEndpointContext<'_>,
1005        payment_hash: bitcoin_hashes::sha256::Hash,
1006    ) -> IncomingContractOffer {
1007        let future = context.wait_key_exists(OfferKey(payment_hash));
1008        future.await
1009    }
1010
1011    async fn get_contract_account(
1012        &self,
1013        dbtx: &mut DatabaseTransaction<'_>,
1014        contract_id: ContractId,
1015    ) -> Option<ContractAccount> {
1016        dbtx.get_value(&ContractKey(contract_id)).await
1017    }
1018
1019    async fn wait_contract_account(
1020        &self,
1021        context: &mut ApiEndpointContext<'_>,
1022        contract_id: ContractId,
1023    ) -> ContractAccount {
1024        // not using a variable here leads to a !Send error
1025        let future = context.wait_key_exists(ContractKey(contract_id));
1026        future.await
1027    }
1028
1029    async fn wait_outgoing_contract_account_cancelled(
1030        &self,
1031        context: &mut ApiEndpointContext<'_>,
1032        contract_id: ContractId,
1033    ) -> ContractAccount {
1034        let future =
1035            context.wait_value_matches(ContractKey(contract_id), |contract| {
1036                match &contract.contract {
1037                    FundedContract::Outgoing(c) => c.cancelled,
1038                    FundedContract::Incoming(_) => false,
1039                }
1040            });
1041        future.await
1042    }
1043
1044    async fn get_decrypted_preimage_status(
1045        &self,
1046        context: &mut ApiEndpointContext<'_>,
1047        contract_id: ContractId,
1048    ) -> (IncomingContractAccount, DecryptedPreimageStatus) {
1049        let f_contract = context.wait_key_exists(ContractKey(contract_id));
1050        let contract = f_contract.await;
1051        let incoming_contract_account = Self::get_incoming_contract_account(contract);
1052        match &incoming_contract_account.contract.decrypted_preimage {
1053            DecryptedPreimage::Some(key) => (
1054                incoming_contract_account.clone(),
1055                DecryptedPreimageStatus::Some(Preimage(sha256::Hash::hash(&key.0).to_byte_array())),
1056            ),
1057            DecryptedPreimage::Pending => {
1058                (incoming_contract_account, DecryptedPreimageStatus::Pending)
1059            }
1060            DecryptedPreimage::Invalid => {
1061                (incoming_contract_account, DecryptedPreimageStatus::Invalid)
1062            }
1063        }
1064    }
1065
1066    async fn wait_preimage_decrypted(
1067        &self,
1068        context: &mut ApiEndpointContext<'_>,
1069        contract_id: ContractId,
1070    ) -> (IncomingContractAccount, Option<Preimage>) {
1071        let future =
1072            context.wait_value_matches(ContractKey(contract_id), |contract| {
1073                match &contract.contract {
1074                    FundedContract::Incoming(c) => match c.contract.decrypted_preimage {
1075                        DecryptedPreimage::Pending => false,
1076                        DecryptedPreimage::Some(_) | DecryptedPreimage::Invalid => true,
1077                    },
1078                    FundedContract::Outgoing(_) => false,
1079                }
1080            });
1081
1082        let decrypt_preimage = future.await;
1083        let incoming_contract_account = Self::get_incoming_contract_account(decrypt_preimage);
1084        match incoming_contract_account
1085            .clone()
1086            .contract
1087            .decrypted_preimage
1088        {
1089            DecryptedPreimage::Some(key) => (
1090                incoming_contract_account,
1091                Some(Preimage(sha256::Hash::hash(&key.0).to_byte_array())),
1092            ),
1093            _ => (incoming_contract_account, None),
1094        }
1095    }
1096
1097    fn get_incoming_contract_account(contract: ContractAccount) -> IncomingContractAccount {
1098        if let FundedContract::Incoming(incoming) = contract.contract {
1099            return IncomingContractAccount {
1100                amount: contract.amount,
1101                contract: incoming.contract,
1102            };
1103        }
1104
1105        panic!("Contract is not an IncomingContractAccount");
1106    }
1107
1108    async fn list_gateways(
1109        &self,
1110        dbtx: &mut DatabaseTransaction<'_>,
1111    ) -> Vec<LightningGatewayAnnouncement> {
1112        let stream = dbtx.find_by_prefix(&LightningGatewayKeyPrefix).await;
1113        stream
1114            .filter_map(|(_, gw)| async { if gw.is_expired() { None } else { Some(gw) } })
1115            .collect::<Vec<LightningGatewayRegistration>>()
1116            .await
1117            .into_iter()
1118            .map(LightningGatewayRegistration::unanchor)
1119            .collect::<Vec<LightningGatewayAnnouncement>>()
1120    }
1121
1122    async fn register_gateway(
1123        &self,
1124        dbtx: &mut DatabaseTransaction<'_>,
1125        gateway: LightningGatewayAnnouncement,
1126    ) {
1127        // Garbage collect expired gateways (since we're already writing to the DB)
1128        // Note: A "gotcha" of doing this here is that if two gateways are registered
1129        // at the same time, they will both attempt to delete the same expired gateways
1130        // and one of them will fail. This should be fine, since the other one will
1131        // succeed and the failed one will just try again.
1132        self.delete_expired_gateways(dbtx).await;
1133
1134        dbtx.insert_entry(
1135            &LightningGatewayKey(gateway.info.gateway_id),
1136            &gateway.anchor(),
1137        )
1138        .await;
1139    }
1140
1141    async fn delete_expired_gateways(&self, dbtx: &mut DatabaseTransaction<'_>) {
1142        let expired_gateway_keys = dbtx
1143            .find_by_prefix(&LightningGatewayKeyPrefix)
1144            .await
1145            .filter_map(|(key, gw)| async move { if gw.is_expired() { Some(key) } else { None } })
1146            .collect::<Vec<LightningGatewayKey>>()
1147            .await;
1148
1149        for key in expired_gateway_keys {
1150            dbtx.remove_entry(&key).await;
1151        }
1152    }
1153
1154    /// Returns the challenge to the gateway that must be signed by the
1155    /// gateway's private key in order for the gateway registration record
1156    /// to be removed. The challenge is the concatenation of the gateway's
1157    /// public key and the `valid_until` bytes. This ensures that the
1158    /// challenges changes every time the gateway is re-registered and ensures
1159    /// that the challenge is unique per-gateway.
1160    async fn get_gateway_remove_challenge(
1161        &self,
1162        gateway_id: PublicKey,
1163        dbtx: &mut DatabaseTransaction<'_>,
1164    ) -> Option<sha256::Hash> {
1165        match dbtx.get_value(&LightningGatewayKey(gateway_id)).await {
1166            Some(gateway) => {
1167                let mut valid_until_bytes = gateway.valid_until.to_bytes();
1168                let mut challenge_bytes = gateway_id.to_bytes();
1169                challenge_bytes.append(&mut valid_until_bytes);
1170                Some(sha256::Hash::hash(&challenge_bytes))
1171            }
1172            _ => None,
1173        }
1174    }
1175
1176    /// Removes the gateway registration record. First the signature provided by
1177    /// the gateway is verified by checking if the gateway's challenge has
1178    /// been signed by the gateway's private key.
1179    async fn remove_gateway(
1180        &self,
1181        remove_gateway_request: RemoveGatewayRequest,
1182        dbtx: &mut DatabaseTransaction<'_>,
1183    ) -> anyhow::Result<()> {
1184        let fed_public_key = self.cfg.consensus.threshold_pub_keys.public_key();
1185        let gateway_id = remove_gateway_request.gateway_id;
1186        let our_peer_id = self.our_peer_id;
1187        let signature = remove_gateway_request
1188            .signatures
1189            .get(&our_peer_id)
1190            .ok_or_else(|| {
1191                warn!(target: LOG_MODULE_LN, "No signature provided for gateway: {gateway_id}");
1192                anyhow::anyhow!("No signature provided for gateway {gateway_id}")
1193            })?;
1194
1195        // If there is no challenge, the gateway does not exist in the database and
1196        // there is nothing to do
1197        let challenge = self
1198            .get_gateway_remove_challenge(gateway_id, dbtx)
1199            .await
1200            .ok_or(anyhow::anyhow!(
1201                "Gateway {gateway_id} is not registered with peer {our_peer_id}"
1202            ))?;
1203
1204        // Verify the supplied schnorr signature is valid
1205        let msg = create_gateway_remove_message(fed_public_key, our_peer_id, challenge);
1206        signature.verify(&msg, &gateway_id.x_only_public_key().0)?;
1207
1208        dbtx.remove_entry(&LightningGatewayKey(gateway_id)).await;
1209        info!(target: LOG_MODULE_LN, "Successfully removed gateway: {gateway_id}");
1210        Ok(())
1211    }
1212}
1213
1214fn record_funded_contract_metric(updated_contract_account: &ContractAccount) {
1215    LN_FUNDED_CONTRACT_SATS
1216        .with_label_values(&[match updated_contract_account.contract {
1217            FundedContract::Incoming(_) => "incoming",
1218            FundedContract::Outgoing(_) => "outgoing",
1219        }])
1220        .observe(updated_contract_account.amount.sats_f64());
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use std::time::Duration;
1226
1227    use assert_matches::assert_matches;
1228    use bitcoin_hashes::{Hash as BitcoinHash, sha256};
1229    use fedimint_core::bitcoin::{Block, BlockHash};
1230    use fedimint_core::db::mem_impl::MemDatabase;
1231    use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
1232    use fedimint_core::encoding::Encodable;
1233    use fedimint_core::envs::BitcoinRpcConfig;
1234    use fedimint_core::module::registry::ModuleRegistry;
1235    use fedimint_core::module::{Amounts, InputMeta, TransactionItemAmounts};
1236    use fedimint_core::secp256k1::{PublicKey, generate_keypair};
1237    use fedimint_core::task::TaskGroup;
1238    use fedimint_core::util::SafeUrl;
1239    use fedimint_core::{Amount, Feerate, InPoint, OutPoint, PeerId, TransactionId};
1240    use fedimint_ln_common::config::{LightningClientConfig, LightningConfig, Network};
1241    use fedimint_ln_common::contracts::incoming::{
1242        FundedIncomingContract, IncomingContract, IncomingContractOffer,
1243    };
1244    use fedimint_ln_common::contracts::outgoing::OutgoingContract;
1245    use fedimint_ln_common::contracts::{
1246        DecryptedPreimage, EncryptedPreimage, FundedContract, IdentifiableContract, Preimage,
1247        PreimageKey,
1248    };
1249    use fedimint_ln_common::{ContractAccount, LightningInput, LightningOutput};
1250    use fedimint_server_core::bitcoin_rpc::{IServerBitcoinRpc, ServerBitcoinRpcMonitor};
1251    use fedimint_server_core::{ServerModule, ServerModuleInit};
1252    use rand::rngs::OsRng;
1253
1254    use crate::db::{ContractKey, LightningAuditItemKey};
1255    use crate::{Lightning, LightningInit};
1256
1257    #[derive(Debug)]
1258    struct MockBitcoinServerRpc;
1259
1260    #[async_trait::async_trait]
1261    impl IServerBitcoinRpc for MockBitcoinServerRpc {
1262        fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
1263            BitcoinRpcConfig {
1264                kind: "mock".to_string(),
1265                url: "http://mock".parse().unwrap(),
1266            }
1267        }
1268
1269        fn get_url(&self) -> SafeUrl {
1270            "http://mock".parse().unwrap()
1271        }
1272
1273        async fn get_network(&self) -> anyhow::Result<Network> {
1274            Err(anyhow::anyhow!("Mock network error"))
1275        }
1276
1277        async fn get_block_count(&self) -> anyhow::Result<u64> {
1278            Err(anyhow::anyhow!("Mock block count error"))
1279        }
1280
1281        async fn get_block_hash(&self, _height: u64) -> anyhow::Result<BlockHash> {
1282            Err(anyhow::anyhow!("Mock block hash error"))
1283        }
1284
1285        async fn get_block(&self, _block_hash: &BlockHash) -> anyhow::Result<Block> {
1286            Err(anyhow::anyhow!("Mock block error"))
1287        }
1288
1289        async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
1290            Err(anyhow::anyhow!("Mock feerate error"))
1291        }
1292
1293        async fn submit_transaction(&self, _transaction: fedimint_core::bitcoin::Transaction) {
1294            // No-op for mock
1295        }
1296
1297        async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
1298            Err(anyhow::anyhow!("Mock sync percentage error"))
1299        }
1300    }
1301
1302    const MINTS: u16 = 4;
1303
1304    fn build_configs() -> (Vec<LightningConfig>, LightningClientConfig) {
1305        let peers = (0..MINTS).map(PeerId::from).collect::<Vec<_>>();
1306        let args = fedimint_server_core::ConfigGenModuleArgs {
1307            network: Network::Regtest,
1308            disable_base_fees: false,
1309        };
1310        let server_cfg = ServerModuleInit::trusted_dealer_gen(&LightningInit, &peers, &args);
1311
1312        let client_cfg = ServerModuleInit::get_client_config(
1313            &LightningInit,
1314            &server_cfg[&PeerId::from(0)].consensus,
1315        )
1316        .unwrap();
1317
1318        let server_cfg = server_cfg
1319            .into_values()
1320            .map(|config| {
1321                config
1322                    .to_typed()
1323                    .expect("Config was just generated by the same configgen")
1324            })
1325            .collect::<Vec<LightningConfig>>();
1326
1327        (server_cfg, client_cfg)
1328    }
1329
1330    fn random_pub_key() -> PublicKey {
1331        generate_keypair(&mut OsRng).1
1332    }
1333
1334    #[test_log::test(tokio::test)]
1335    async fn encrypted_preimage_only_usable_once() {
1336        let task_group = TaskGroup::new();
1337        let (server_cfg, client_cfg) = build_configs();
1338
1339        let server = Lightning {
1340            cfg: server_cfg[0].clone(),
1341            our_peer_id: 0.into(),
1342            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1343                MockBitcoinServerRpc.into_dyn(),
1344                Duration::from_secs(1),
1345                &task_group,
1346            ),
1347        };
1348
1349        let preimage = [42u8; 32];
1350        let encrypted_preimage = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([42; 32]));
1351
1352        let hash = preimage.consensus_hash();
1353        let offer = IncomingContractOffer {
1354            amount: Amount::from_sats(10),
1355            hash,
1356            encrypted_preimage: encrypted_preimage.clone(),
1357            expiry_time: None,
1358        };
1359        let output = LightningOutput::new_v0_offer(offer);
1360        let out_point = OutPoint {
1361            txid: TransactionId::all_zeros(),
1362            out_idx: 0,
1363        };
1364
1365        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1366        let mut dbtx = db.begin_transaction_nc().await;
1367
1368        server
1369            .process_output(
1370                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1371                &output,
1372                out_point,
1373            )
1374            .await
1375            .expect("First time works");
1376
1377        let hash2 = [21u8, 32].consensus_hash();
1378        let offer2 = IncomingContractOffer {
1379            amount: Amount::from_sats(1),
1380            hash: hash2,
1381            encrypted_preimage,
1382            expiry_time: None,
1383        };
1384        let output2 = LightningOutput::new_v0_offer(offer2);
1385        let out_point2 = OutPoint {
1386            txid: TransactionId::all_zeros(),
1387            out_idx: 1,
1388        };
1389
1390        assert_matches!(
1391            server
1392                .process_output(
1393                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1394                    &output2,
1395                    out_point2
1396                )
1397                .await,
1398            Err(_)
1399        );
1400    }
1401
1402    #[test_log::test(tokio::test)]
1403    async fn process_input_for_valid_incoming_contracts() {
1404        let task_group = TaskGroup::new();
1405        let (server_cfg, client_cfg) = build_configs();
1406        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1407        let mut dbtx = db.begin_transaction_nc().await;
1408        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
1409
1410        let server = Lightning {
1411            cfg: server_cfg[0].clone(),
1412            our_peer_id: 0.into(),
1413            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1414                MockBitcoinServerRpc.into_dyn(),
1415                Duration::from_secs(1),
1416                &task_group,
1417            ),
1418        };
1419
1420        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
1421        let funded_incoming_contract = FundedContract::Incoming(FundedIncomingContract {
1422            contract: IncomingContract {
1423                hash: sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array()),
1424                encrypted_preimage: EncryptedPreimage(
1425                    client_cfg.threshold_pub_key.encrypt(preimage.0),
1426                ),
1427                decrypted_preimage: DecryptedPreimage::Some(preimage.clone()),
1428                gateway_key: random_pub_key(),
1429            },
1430            out_point: OutPoint {
1431                txid: TransactionId::all_zeros(),
1432                out_idx: 0,
1433            },
1434        });
1435
1436        let contract_id = funded_incoming_contract.contract_id();
1437        let audit_key = LightningAuditItemKey::from_funded_contract(&funded_incoming_contract);
1438        let amount = Amount { msats: 1000 };
1439        let lightning_input = LightningInput::new_v0(contract_id, amount, None);
1440
1441        module_dbtx.insert_new_entry(&audit_key, &amount).await;
1442        module_dbtx
1443            .insert_new_entry(
1444                &ContractKey(contract_id),
1445                &ContractAccount {
1446                    amount,
1447                    contract: funded_incoming_contract,
1448                },
1449            )
1450            .await;
1451
1452        let processed_input_meta = server
1453            .process_input(
1454                &mut module_dbtx.to_ref_nc(),
1455                &lightning_input,
1456                InPoint {
1457                    txid: TransactionId::all_zeros(),
1458                    in_idx: 0,
1459                },
1460            )
1461            .await
1462            .expect("should process valid incoming contract");
1463        let expected_input_meta = InputMeta {
1464            amount: TransactionItemAmounts {
1465                amounts: Amounts::new_bitcoin(amount),
1466                fees: Amounts::ZERO,
1467            },
1468            pub_key: preimage
1469                .to_public_key()
1470                .expect("should create Schnorr pubkey from preimage"),
1471        };
1472
1473        assert_eq!(processed_input_meta, expected_input_meta);
1474
1475        let audit_item = module_dbtx.get_value(&audit_key).await;
1476        assert_eq!(audit_item, None);
1477    }
1478
1479    #[test_log::test(tokio::test)]
1480    async fn process_input_for_valid_outgoing_contracts() {
1481        let task_group = TaskGroup::new();
1482        let (server_cfg, _) = build_configs();
1483        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1484        let mut dbtx = db.begin_transaction_nc().await;
1485        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
1486
1487        let server = Lightning {
1488            cfg: server_cfg[0].clone(),
1489            our_peer_id: 0.into(),
1490            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1491                MockBitcoinServerRpc.into_dyn(),
1492                Duration::from_secs(1),
1493                &task_group,
1494            ),
1495        };
1496
1497        let preimage = Preimage([42u8; 32]);
1498        let gateway_key = random_pub_key();
1499        let outgoing_contract = FundedContract::Outgoing(OutgoingContract {
1500            hash: preimage.consensus_hash(),
1501            gateway_key,
1502            timelock: 1_000_000,
1503            user_key: random_pub_key(),
1504            cancelled: false,
1505        });
1506        let contract_id = outgoing_contract.contract_id();
1507        let audit_key = LightningAuditItemKey::from_funded_contract(&outgoing_contract);
1508        let amount = Amount { msats: 1000 };
1509        let lightning_input = LightningInput::new_v0(contract_id, amount, Some(preimage.clone()));
1510
1511        module_dbtx.insert_new_entry(&audit_key, &amount).await;
1512        module_dbtx
1513            .insert_new_entry(
1514                &ContractKey(contract_id),
1515                &ContractAccount {
1516                    amount,
1517                    contract: outgoing_contract,
1518                },
1519            )
1520            .await;
1521
1522        let processed_input_meta = server
1523            .process_input(
1524                &mut module_dbtx.to_ref_nc(),
1525                &lightning_input,
1526                InPoint {
1527                    txid: TransactionId::all_zeros(),
1528                    in_idx: 0,
1529                },
1530            )
1531            .await
1532            .expect("should process valid outgoing contract");
1533
1534        let expected_input_meta = InputMeta {
1535            amount: TransactionItemAmounts {
1536                amounts: Amounts::new_bitcoin(amount),
1537                fees: Amounts::ZERO,
1538            },
1539            pub_key: gateway_key,
1540        };
1541
1542        assert_eq!(processed_input_meta, expected_input_meta);
1543
1544        let audit_item = module_dbtx.get_value(&audit_key).await;
1545        assert_eq!(audit_item, None);
1546    }
1547}