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