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