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