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