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