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