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