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