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