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