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