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