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