1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::module_name_repetitions)]
4
5pub use fedimint_lnv2_common as common;
6
7pub mod db;
8mod metrics;
9
10use std::collections::{BTreeMap, BTreeSet};
11use std::time::Duration;
12
13use anyhow::{Context, anyhow, ensure};
14use bls12_381::{G1Projective, Scalar};
15use fedimint_core::config::{
16 ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
17 TypedServerModuleConsensusConfig,
18};
19use fedimint_core::core::ModuleInstanceId;
20use fedimint_core::db::{
21 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
22};
23use fedimint_core::envs::{FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set_opt};
24use fedimint_core::module::audit::Audit;
25use fedimint_core::module::{
26 Amounts, ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta,
27 ModuleConsensusVersion, ModuleInit, TransactionItemAmounts, admin_api_endpoint,
28 public_api_endpoint,
29};
30use fedimint_core::task::timeout;
31use fedimint_core::time::duration_since_epoch;
32use fedimint_core::util::SafeUrl;
33use fedimint_core::{
34 Amount, InPoint, NumPeers, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send,
35 push_db_pair_items,
36};
37use fedimint_lnv2_common::config::{
38 FeeConsensus, LightningClientConfig, LightningConfig, LightningConfigConsensus,
39 LightningConfigPrivate,
40};
41use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract};
42use fedimint_lnv2_common::endpoint_constants::{
43 ADD_GATEWAY_ENDPOINT, AWAIT_INCOMING_CONTRACT_ENDPOINT, AWAIT_INCOMING_CONTRACTS_ENDPOINT,
44 AWAIT_PREIMAGE_ENDPOINT, CONSENSUS_BLOCK_COUNT_ENDPOINT, DECRYPTION_KEY_SHARE_ENDPOINT,
45 GATEWAYS_ENDPOINT, OUTGOING_CONTRACT_EXPIRATION_ENDPOINT, REMOVE_GATEWAY_ENDPOINT,
46};
47use fedimint_lnv2_common::{
48 ContractId, LightningCommonInit, LightningConsensusItem, LightningInput, LightningInputError,
49 LightningInputV0, LightningModuleTypes, LightningOutput, LightningOutputError,
50 LightningOutputOutcome, LightningOutputV0, MODULE_CONSENSUS_VERSION, OutgoingWitness,
51};
52use fedimint_logging::LOG_MODULE_LNV2;
53use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
54use fedimint_server_core::config::{PeerHandleOps, eval_poly_g1};
55use fedimint_server_core::migration::ServerModuleDbMigrationFn;
56use fedimint_server_core::{
57 ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
58};
59use futures::StreamExt;
60use group::Curve;
61use group::ff::Field;
62use rand::rngs::OsRng;
63use strum::IntoEnumIterator;
64use tpe::{
65 AggregatePublicKey, DecryptionKeyShare, PublicKeyShare, SecretKeyShare, derive_pk_share,
66};
67use tracing::trace;
68
69use crate::db::{
70 BlockCountVoteKey, BlockCountVotePrefix, DbKeyPrefix, DecryptionKeyShareKey,
71 DecryptionKeySharePrefix, GatewayKey, GatewayPrefix, IncomingContractIndexKey,
72 IncomingContractIndexPrefix, IncomingContractKey, IncomingContractOutpointKey,
73 IncomingContractOutpointPrefix, IncomingContractPrefix, IncomingContractStreamIndexKey,
74 IncomingContractStreamKey, IncomingContractStreamPrefix, OutgoingContractKey,
75 OutgoingContractPrefix, PreimageKey, PreimagePrefix, UnixTimeVoteKey, UnixTimeVotePrefix,
76};
77use crate::metrics::{LN_FUNDED_CONTRACT_SATS, LN_OUTGOING_CONTRACT_SETTLED};
78
79const MAX_INCOMING_CONTRACTS_BATCH: usize = 1024;
86
87#[derive(Debug, Clone)]
88pub struct LightningInit;
89
90impl ModuleInit for LightningInit {
91 type Common = LightningCommonInit;
92
93 #[allow(clippy::too_many_lines)]
94 async fn dump_database(
95 &self,
96 dbtx: &mut DatabaseTransaction<'_>,
97 prefix_names: Vec<String>,
98 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
99 let mut lightning: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
100 BTreeMap::new();
101
102 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
103 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
104 });
105
106 for table in filtered_prefixes {
107 match table {
108 DbKeyPrefix::BlockCountVote => {
109 push_db_pair_items!(
110 dbtx,
111 BlockCountVotePrefix,
112 BlockCountVoteKey,
113 u64,
114 lightning,
115 "Lightning Block Count Votes"
116 );
117 }
118 DbKeyPrefix::UnixTimeVote => {
119 push_db_pair_items!(
120 dbtx,
121 UnixTimeVotePrefix,
122 UnixTimeVoteKey,
123 u64,
124 lightning,
125 "Lightning Unix Time Votes"
126 );
127 }
128 DbKeyPrefix::OutgoingContract => {
129 push_db_pair_items!(
130 dbtx,
131 OutgoingContractPrefix,
132 LightningOutgoingContractKey,
133 OutgoingContract,
134 lightning,
135 "Lightning Outgoing Contracts"
136 );
137 }
138 DbKeyPrefix::IncomingContract => {
139 push_db_pair_items!(
140 dbtx,
141 IncomingContractPrefix,
142 LightningIncomingContractKey,
143 IncomingContract,
144 lightning,
145 "Lightning Incoming Contracts"
146 );
147 }
148 DbKeyPrefix::IncomingContractOutpoint => {
149 push_db_pair_items!(
150 dbtx,
151 IncomingContractOutpointPrefix,
152 LightningIncomingContractOutpointKey,
153 OutPoint,
154 lightning,
155 "Lightning Incoming Contracts Outpoints"
156 );
157 }
158 DbKeyPrefix::DecryptionKeyShare => {
159 push_db_pair_items!(
160 dbtx,
161 DecryptionKeySharePrefix,
162 DecryptionKeyShareKey,
163 DecryptionKeyShare,
164 lightning,
165 "Lightning Decryption Key Share"
166 );
167 }
168 DbKeyPrefix::Preimage => {
169 push_db_pair_items!(
170 dbtx,
171 PreimagePrefix,
172 LightningPreimageKey,
173 [u8; 32],
174 lightning,
175 "Lightning Preimages"
176 );
177 }
178 DbKeyPrefix::Gateway => {
179 push_db_pair_items!(
180 dbtx,
181 GatewayPrefix,
182 GatewayKey,
183 (),
184 lightning,
185 "Lightning Gateways"
186 );
187 }
188 DbKeyPrefix::IncomingContractStreamIndex => {
189 push_db_pair_items!(
190 dbtx,
191 IncomingContractStreamIndexKey,
192 IncomingContractStreamIndexKey,
193 u64,
194 lightning,
195 "Lightning Incoming Contract Stream Index"
196 );
197 }
198 DbKeyPrefix::IncomingContractStream => {
199 push_db_pair_items!(
200 dbtx,
201 IncomingContractStreamPrefix(0),
202 IncomingContractStreamKey,
203 IncomingContract,
204 lightning,
205 "Lightning Incoming Contract Stream"
206 );
207 }
208 DbKeyPrefix::IncomingContractIndex => {
209 push_db_pair_items!(
210 dbtx,
211 IncomingContractIndexPrefix,
212 IncomingContractIndexKey,
213 u64,
214 lightning,
215 "Lightning Incoming Contract Index"
216 );
217 }
218 }
219 }
220
221 Box::new(lightning.into_iter())
222 }
223}
224
225#[apply(async_trait_maybe_send!)]
226impl ServerModuleInit for LightningInit {
227 type Module = Lightning;
228
229 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
230 &[MODULE_CONSENSUS_VERSION]
231 }
232
233 fn is_enabled_by_default(&self) -> bool {
234 is_env_var_set_opt(FM_ENABLE_MODULE_LNV2_ENV).unwrap_or(true)
235 }
236
237 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
238 vec![EnvVarDoc {
239 name: FM_ENABLE_MODULE_LNV2_ENV,
240 description: "Set to 0/false to disable the LNv2 Lightning module. Enabled by default.",
241 }]
242 }
243
244 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
245 for direction in ["incoming", "outgoing"] {
247 LN_FUNDED_CONTRACT_SATS
248 .with_label_values(&[direction])
249 .get_sample_count();
250 }
251 for outcome in ["claim", "refund", "cancel"] {
252 LN_OUTGOING_CONTRACT_SETTLED
253 .with_label_values(&[outcome])
254 .get();
255 }
256
257 Ok(Lightning {
258 cfg: args.cfg().to_typed()?,
259 db: args.db().clone(),
260 server_bitcoin_rpc_monitor: args.server_bitcoin_rpc_monitor(),
261 })
262 }
263
264 fn trusted_dealer_gen(
265 &self,
266 peers: &[PeerId],
267 args: &ConfigGenModuleArgs,
268 ) -> BTreeMap<PeerId, ServerModuleConfig> {
269 let polynomial = dealer_polynomial(peers.to_num_peers());
270
271 let tpe_pks = peers
272 .iter()
273 .map(|peer| (*peer, dealer_pk(&polynomial, *peer)))
274 .collect::<BTreeMap<PeerId, PublicKeyShare>>();
275
276 peers
277 .iter()
278 .map(|peer| {
279 let cfg = LightningConfig {
280 consensus: LightningConfigConsensus {
281 tpe_agg_pk: dealer_agg_pk(&polynomial),
282 tpe_pks: tpe_pks.clone(),
283 fee_consensus: if args.disable_base_fees {
284 FeeConsensus::zero()
285 } else {
286 FeeConsensus::new(0).expect("Relative fee is within range")
287 },
288 network: args.network,
289 },
290 private: LightningConfigPrivate {
291 sk: dealer_sk(&polynomial, *peer),
292 },
293 };
294
295 (*peer, cfg.to_erased())
296 })
297 .collect()
298 }
299
300 async fn distributed_gen(
301 &self,
302 peers: &(dyn PeerHandleOps + Send + Sync),
303 args: &ConfigGenModuleArgs,
304 ) -> anyhow::Result<ServerModuleConfig> {
305 let (polynomial, sks) = peers.run_dkg_g1().await?;
306
307 let server = LightningConfig {
308 consensus: LightningConfigConsensus {
309 tpe_agg_pk: tpe::AggregatePublicKey(polynomial[0].to_affine()),
310 tpe_pks: peers
311 .num_peers()
312 .peer_ids()
313 .map(|peer| (peer, PublicKeyShare(eval_poly_g1(&polynomial, &peer))))
314 .collect(),
315 fee_consensus: if args.disable_base_fees {
316 FeeConsensus::zero()
317 } else {
318 FeeConsensus::new(0).expect("Relative fee is within range")
319 },
320 network: args.network,
321 },
322 private: LightningConfigPrivate {
323 sk: SecretKeyShare(sks),
324 },
325 };
326
327 Ok(server.to_erased())
328 }
329
330 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
331 let config = config.to_typed::<LightningConfig>()?;
332
333 ensure!(
334 tpe::derive_pk_share(&config.private.sk)
335 == *config
336 .consensus
337 .tpe_pks
338 .get(identity)
339 .context("Public key set has no key for our identity")?,
340 "Preimge encryption secret key share does not match our public key share"
341 );
342
343 Ok(())
344 }
345
346 fn get_client_config(
347 &self,
348 config: &ServerModuleConsensusConfig,
349 ) -> anyhow::Result<LightningClientConfig> {
350 let config = LightningConfigConsensus::from_erased(config)?;
351 Ok(LightningClientConfig {
352 tpe_agg_pk: config.tpe_agg_pk,
353 tpe_pks: config.tpe_pks,
354 fee_consensus: config.fee_consensus,
355 network: config.network,
356 })
357 }
358
359 fn get_database_migrations(
360 &self,
361 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Lightning>> {
362 let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Lightning>> =
363 BTreeMap::new();
364
365 migrations.insert(
366 DatabaseVersion(0),
367 Box::new(move |ctx| Box::pin(crate::db::migrate_to_v1(ctx))),
368 );
369
370 migrations
371 }
372
373 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
374 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
375 }
376}
377
378fn dealer_polynomial(num_peers: NumPeers) -> Vec<Scalar> {
379 (0..num_peers.threshold())
380 .map(|_| Scalar::random(&mut OsRng))
381 .collect()
382}
383
384fn dealer_agg_pk(polynomial: &[Scalar]) -> AggregatePublicKey {
385 AggregatePublicKey((G1Projective::generator() * polynomial[0]).to_affine())
386}
387
388fn dealer_pk(polynomial: &[Scalar], peer: PeerId) -> PublicKeyShare {
389 derive_pk_share(&dealer_sk(polynomial, peer))
390}
391
392fn dealer_sk(polynomial: &[Scalar], peer: PeerId) -> SecretKeyShare {
393 let x = Scalar::from(peer.to_usize() as u64 + 1);
394
395 let y = polynomial
399 .iter()
400 .copied()
401 .rev()
402 .reduce(|accumulator, c| accumulator * x + c)
403 .expect("We have at least one coefficient");
404
405 SecretKeyShare(y)
406}
407
408#[derive(Debug)]
409pub struct Lightning {
410 cfg: LightningConfig,
411 db: Database,
412 server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
413}
414
415#[apply(async_trait_maybe_send!)]
416impl ServerModule for Lightning {
417 type Common = LightningModuleTypes;
418 type Init = LightningInit;
419
420 async fn consensus_proposal(
421 &self,
422 _dbtx: &mut DatabaseTransaction<'_>,
423 ) -> Vec<LightningConsensusItem> {
424 let mut items = vec![LightningConsensusItem::UnixTimeVote(
427 60 * (duration_since_epoch().as_secs() / 60),
428 )];
429
430 if let Ok(block_count) = self.get_block_count() {
431 trace!(target: LOG_MODULE_LNV2, ?block_count, "Proposing block count");
432 items.push(LightningConsensusItem::BlockCountVote(block_count));
433 }
434
435 items
436 }
437
438 async fn process_consensus_item<'a, 'b>(
439 &'a self,
440 dbtx: &mut DatabaseTransaction<'b>,
441 consensus_item: LightningConsensusItem,
442 peer: PeerId,
443 ) -> anyhow::Result<()> {
444 trace!(target: LOG_MODULE_LNV2, ?consensus_item, "Processing consensus item proposal");
445
446 match consensus_item {
447 LightningConsensusItem::BlockCountVote(vote) => {
448 let current_vote = dbtx
449 .insert_entry(&BlockCountVoteKey(peer), &vote)
450 .await
451 .unwrap_or(0);
452
453 ensure!(current_vote < vote, "Block count vote is redundant");
454
455 Ok(())
456 }
457 LightningConsensusItem::UnixTimeVote(vote) => {
458 let current_vote = dbtx
459 .insert_entry(&UnixTimeVoteKey(peer), &vote)
460 .await
461 .unwrap_or(0);
462
463 ensure!(current_vote < vote, "Unix time vote is redundant");
464
465 Ok(())
466 }
467 LightningConsensusItem::Default { variant, .. } => Err(anyhow!(
468 "Received lnv2 consensus item with unknown variant {variant}"
469 )),
470 }
471 }
472
473 async fn process_input<'a, 'b, 'c>(
474 &'a self,
475 dbtx: &mut DatabaseTransaction<'c>,
476 input: &'b LightningInput,
477 _in_point: InPoint,
478 ) -> Result<InputMeta, LightningInputError> {
479 let (pub_key, amount) = match input.ensure_v0_ref()? {
480 LightningInputV0::Outgoing(outpoint, outgoing_witness) => {
481 let contract = dbtx
482 .remove_entry(&OutgoingContractKey(*outpoint))
483 .await
484 .ok_or(LightningInputError::UnknownContract)?;
485
486 let pub_key = match outgoing_witness {
487 OutgoingWitness::Claim(preimage) => {
488 if contract.expiration <= self.consensus_block_count(dbtx).await {
489 return Err(LightningInputError::Expired);
490 }
491
492 if !contract.verify_preimage(preimage) {
493 return Err(LightningInputError::InvalidPreimage);
494 }
495
496 dbtx.insert_entry(&PreimageKey(*outpoint), preimage).await;
497
498 contract.claim_pk
499 }
500 OutgoingWitness::Refund => {
501 if contract.expiration > self.consensus_block_count(dbtx).await {
502 return Err(LightningInputError::NotExpired);
503 }
504
505 contract.refund_pk
506 }
507 OutgoingWitness::Cancel(forfeit_signature) => {
508 if !contract.verify_forfeit_signature(forfeit_signature) {
509 return Err(LightningInputError::InvalidForfeitSignature);
510 }
511
512 contract.refund_pk
513 }
514 };
515
516 let outcome = match outgoing_witness {
517 OutgoingWitness::Claim(..) => "claim",
518 OutgoingWitness::Refund => "refund",
519 OutgoingWitness::Cancel(..) => "cancel",
520 };
521
522 dbtx.on_commit(move || {
523 LN_OUTGOING_CONTRACT_SETTLED
524 .with_label_values(&[outcome])
525 .inc();
526 });
527
528 (pub_key, contract.amount)
529 }
530 LightningInputV0::Incoming(outpoint, agg_decryption_key) => {
531 let contract = dbtx
532 .remove_entry(&IncomingContractKey(*outpoint))
533 .await
534 .ok_or(LightningInputError::UnknownContract)?;
535
536 let index = dbtx
537 .remove_entry(&IncomingContractIndexKey(*outpoint))
538 .await
539 .expect("Incoming contract index should exist");
540
541 dbtx.remove_entry(&IncomingContractStreamKey(index)).await;
542
543 if !contract
544 .verify_agg_decryption_key(&self.cfg.consensus.tpe_agg_pk, agg_decryption_key)
545 {
546 return Err(LightningInputError::InvalidDecryptionKey);
547 }
548
549 let pub_key = match contract.decrypt_preimage(agg_decryption_key) {
550 Some(..) => contract.commitment.claim_pk,
551 None => contract.commitment.refund_pk,
552 };
553
554 (pub_key, contract.commitment.amount)
555 }
556 };
557
558 Ok(InputMeta {
559 amount: TransactionItemAmounts {
560 amounts: Amounts::new_bitcoin(amount),
561 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
562 },
563 pub_key,
564 })
565 }
566
567 async fn process_output<'a, 'b>(
568 &'a self,
569 dbtx: &mut DatabaseTransaction<'b>,
570 output: &'a LightningOutput,
571 outpoint: OutPoint,
572 ) -> Result<TransactionItemAmounts, LightningOutputError> {
573 let amount = match output.ensure_v0_ref()? {
574 LightningOutputV0::Outgoing(contract) => {
575 dbtx.insert_new_entry(&OutgoingContractKey(outpoint), contract)
576 .await;
577
578 observe_funded_contract(dbtx, "outgoing", contract.amount);
579
580 contract.amount
581 }
582 LightningOutputV0::Incoming(contract) => {
583 if !contract.verify() {
584 return Err(LightningOutputError::InvalidContract);
585 }
586
587 if contract.commitment.expiration_or_fee <= self.consensus_unix_time(dbtx).await {
588 return Err(LightningOutputError::ContractExpired);
589 }
590
591 dbtx.insert_new_entry(&IncomingContractKey(outpoint), contract)
592 .await;
593
594 dbtx.insert_entry(
595 &IncomingContractOutpointKey(contract.contract_id()),
596 &outpoint,
597 )
598 .await;
599
600 let stream_index = dbtx
601 .get_value(&IncomingContractStreamIndexKey)
602 .await
603 .unwrap_or(0);
604
605 dbtx.insert_entry(&IncomingContractStreamKey(stream_index), contract)
606 .await;
607
608 dbtx.insert_entry(&IncomingContractIndexKey(outpoint), &stream_index)
609 .await;
610
611 dbtx.insert_entry(&IncomingContractStreamIndexKey, &(stream_index + 1))
612 .await;
613
614 let dk_share = contract.create_decryption_key_share(&self.cfg.private.sk);
615
616 dbtx.insert_entry(&DecryptionKeyShareKey(outpoint), &dk_share)
617 .await;
618
619 observe_funded_contract(dbtx, "incoming", contract.commitment.amount);
620
621 contract.commitment.amount
622 }
623 };
624
625 Ok(TransactionItemAmounts {
626 amounts: Amounts::new_bitcoin(amount),
627 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
628 })
629 }
630
631 async fn output_status(
632 &self,
633 _dbtx: &mut DatabaseTransaction<'_>,
634 _out_point: OutPoint,
635 ) -> Option<LightningOutputOutcome> {
636 None
637 }
638
639 async fn audit(
640 &self,
641 dbtx: &mut DatabaseTransaction<'_>,
642 audit: &mut Audit,
643 module_instance_id: ModuleInstanceId,
644 ) {
645 audit
648 .add_items(
649 dbtx,
650 module_instance_id,
651 &OutgoingContractPrefix,
652 |_, contract| -(contract.amount.msats as i64),
653 )
654 .await;
655
656 audit
657 .add_items(
658 dbtx,
659 module_instance_id,
660 &IncomingContractPrefix,
661 |_, contract| -(contract.commitment.amount.msats as i64),
662 )
663 .await;
664 }
665
666 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
667 vec![
668 public_api_endpoint! {
669 CONSENSUS_BLOCK_COUNT_ENDPOINT,
670 ApiVersion::new(0, 0),
671 async |module: &Lightning, context, _params : () | -> u64 {
672 let db = context.db();
673 let mut dbtx = db.begin_transaction_nc().await;
674
675 Ok(module.consensus_block_count(&mut dbtx).await)
676 }
677 },
678 public_api_endpoint! {
679 AWAIT_INCOMING_CONTRACT_ENDPOINT,
680 ApiVersion::new(0, 0),
681 async |module: &Lightning, context, params: (ContractId, u64) | -> Option<OutPoint> {
682 let db = context.db();
683
684 Ok(module.await_incoming_contract(db, params.0, params.1).await)
685 }
686 },
687 public_api_endpoint! {
688 AWAIT_PREIMAGE_ENDPOINT,
689 ApiVersion::new(0, 0),
690 async |module: &Lightning, context, params: (OutPoint, u64)| -> Option<[u8; 32]> {
691 let db = context.db();
692
693 Ok(module.await_preimage(db, params.0, params.1).await)
694 }
695 },
696 public_api_endpoint! {
697 DECRYPTION_KEY_SHARE_ENDPOINT,
698 ApiVersion::new(0, 0),
699 async |_module: &Lightning, context, params: OutPoint| -> DecryptionKeyShare {
700 Ok(context
706 .db()
707 .wait_key_exists(&DecryptionKeyShareKey(params))
708 .await)
709 }
710 },
711 public_api_endpoint! {
712 OUTGOING_CONTRACT_EXPIRATION_ENDPOINT,
713 ApiVersion::new(0, 0),
714 async |module: &Lightning, context, outpoint: OutPoint| -> Option<(ContractId, u64)> {
715 let db = context.db();
716
717 Ok(module.outgoing_contract_expiration(db, outpoint).await)
718 }
719 },
720 public_api_endpoint! {
721 AWAIT_INCOMING_CONTRACTS_ENDPOINT,
722 ApiVersion::new(0, 0),
723 async |module: &Lightning, context, params: (u64, usize)| -> (Vec<IncomingContract>, u64) {
724 let db = context.db();
725
726 if params.1 == 0 || params.1 > MAX_INCOMING_CONTRACTS_BATCH {
727 return Err(ApiError::bad_request(format!(
728 "Batch size must be in 1..={MAX_INCOMING_CONTRACTS_BATCH}"
729 )));
730 }
731
732 Ok(module.await_incoming_contracts(db, params.0, params.1).await)
733 }
734 },
735 admin_api_endpoint! {
736 ADD_GATEWAY_ENDPOINT,
737 ApiVersion::new(0, 0),
738 async |_module: &Lightning, context, gateway: SafeUrl| -> bool {
739
740 let db = context.db();
741
742 Ok(Lightning::add_gateway(db, gateway).await)
743 }
744 },
745 admin_api_endpoint! {
746 REMOVE_GATEWAY_ENDPOINT,
747 ApiVersion::new(0, 0),
748 async |_module: &Lightning, context, gateway: SafeUrl| -> bool {
749
750 let db = context.db();
751
752 Ok(Lightning::remove_gateway(db, gateway).await)
753 }
754 },
755 public_api_endpoint! {
756 GATEWAYS_ENDPOINT,
757 ApiVersion::new(0, 0),
758 async |_module: &Lightning, context, _params : () | -> Vec<SafeUrl> {
759 let db = context.db();
760
761 Ok(Lightning::gateways(db).await)
762 }
763 },
764 ]
765 }
766}
767
768impl Lightning {
769 fn get_block_count(&self) -> anyhow::Result<u64> {
770 self.server_bitcoin_rpc_monitor
771 .status()
772 .map(|status| status.block_count)
773 .context("Block count not available yet")
774 }
775
776 async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
777 let num_peers = self.cfg.consensus.tpe_pks.to_num_peers();
778
779 let mut counts = dbtx
780 .find_by_prefix(&BlockCountVotePrefix)
781 .await
782 .map(|entry| entry.1)
783 .collect::<Vec<u64>>()
784 .await;
785
786 counts.sort_unstable();
787
788 counts.reverse();
789
790 assert!(counts.last() <= counts.first());
791
792 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
797 }
798
799 async fn consensus_unix_time(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
800 let num_peers = self.cfg.consensus.tpe_pks.to_num_peers();
801
802 let mut times = dbtx
803 .find_by_prefix(&UnixTimeVotePrefix)
804 .await
805 .map(|entry| entry.1)
806 .collect::<Vec<u64>>()
807 .await;
808
809 times.sort_unstable();
810
811 times.reverse();
812
813 assert!(times.last() <= times.first());
814
815 times.get(num_peers.threshold() - 1).copied().unwrap_or(0)
820 }
821
822 async fn await_incoming_contract(
823 &self,
824 db: Database,
825 contract_id: ContractId,
826 expiration: u64,
827 ) -> Option<OutPoint> {
828 loop {
829 timeout(
830 Duration::from_secs(10),
831 db.wait_key_exists(&IncomingContractOutpointKey(contract_id)),
832 )
833 .await
834 .ok();
835
836 let mut dbtx = db.begin_transaction_nc().await;
839
840 if let Some(outpoint) = dbtx
841 .get_value(&IncomingContractOutpointKey(contract_id))
842 .await
843 {
844 return Some(outpoint);
845 }
846
847 if expiration <= self.consensus_unix_time(&mut dbtx).await {
848 return None;
849 }
850 }
851 }
852
853 async fn await_preimage(
854 &self,
855 db: Database,
856 outpoint: OutPoint,
857 expiration: u64,
858 ) -> Option<[u8; 32]> {
859 loop {
860 timeout(
861 Duration::from_secs(10),
862 db.wait_key_exists(&PreimageKey(outpoint)),
863 )
864 .await
865 .ok();
866
867 let mut dbtx = db.begin_transaction_nc().await;
870
871 if let Some(preimage) = dbtx.get_value(&PreimageKey(outpoint)).await {
872 return Some(preimage);
873 }
874
875 if expiration <= self.consensus_block_count(&mut dbtx).await {
876 return None;
877 }
878 }
879 }
880
881 async fn outgoing_contract_expiration(
882 &self,
883 db: Database,
884 outpoint: OutPoint,
885 ) -> Option<(ContractId, u64)> {
886 let mut dbtx = db.begin_transaction_nc().await;
887
888 let contract = dbtx.get_value(&OutgoingContractKey(outpoint)).await?;
889
890 let consensus_block_count = self.consensus_block_count(&mut dbtx).await;
891
892 let expiration = contract.expiration.saturating_sub(consensus_block_count);
893
894 Some((contract.contract_id(), expiration))
895 }
896
897 async fn await_incoming_contracts(
898 &self,
899 db: Database,
900 start: u64,
901 n: usize,
902 ) -> (Vec<IncomingContract>, u64) {
903 let filter = |next_index: Option<u64>| next_index.filter(|i| *i > start);
904
905 let (mut next_index, mut dbtx) = db
906 .wait_key_check(&IncomingContractStreamIndexKey, filter)
907 .await;
908
909 let mut contracts = Vec::with_capacity(n.min(MAX_INCOMING_CONTRACTS_BATCH));
913
914 let range = IncomingContractStreamKey(start)..IncomingContractStreamKey(u64::MAX);
915
916 for (key, contract) in dbtx
917 .find_by_range(range)
918 .await
919 .take(n)
920 .collect::<Vec<(IncomingContractStreamKey, IncomingContract)>>()
921 .await
922 {
923 contracts.push(contract.clone());
924 next_index = key.0 + 1;
925 }
926
927 (contracts, next_index)
928 }
929
930 async fn add_gateway(db: Database, gateway: SafeUrl) -> bool {
931 let mut dbtx = db.begin_transaction().await;
932
933 let is_new_entry = dbtx.insert_entry(&GatewayKey(gateway), &()).await.is_none();
934
935 dbtx.commit_tx().await;
936
937 is_new_entry
938 }
939
940 async fn remove_gateway(db: Database, gateway: SafeUrl) -> bool {
941 let mut dbtx = db.begin_transaction().await;
942
943 let entry_existed = dbtx.remove_entry(&GatewayKey(gateway)).await.is_some();
944
945 dbtx.commit_tx().await;
946
947 entry_existed
948 }
949
950 async fn gateways(db: Database) -> Vec<SafeUrl> {
951 db.begin_transaction_nc()
952 .await
953 .find_by_prefix(&GatewayPrefix)
954 .await
955 .map(|entry| entry.0.0)
956 .collect()
957 .await
958 }
959
960 pub async fn consensus_block_count_ui(&self) -> u64 {
961 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
962 .await
963 }
964
965 pub async fn consensus_unix_time_ui(&self) -> u64 {
966 self.consensus_unix_time(&mut self.db.begin_transaction_nc().await)
967 .await
968 }
969
970 pub async fn add_gateway_ui(&self, gateway: SafeUrl) -> bool {
971 Self::add_gateway(self.db.clone(), gateway).await
972 }
973
974 pub async fn remove_gateway_ui(&self, gateway: SafeUrl) -> bool {
975 Self::remove_gateway(self.db.clone(), gateway).await
976 }
977
978 pub async fn gateways_ui(&self) -> Vec<SafeUrl> {
979 Self::gateways(self.db.clone()).await
980 }
981}
982
983fn observe_funded_contract(
984 dbtx: &mut DatabaseTransaction<'_>,
985 direction: &'static str,
986 amount: Amount,
987) {
988 dbtx.on_commit(move || {
989 LN_FUNDED_CONTRACT_SATS
990 .with_label_values(&[direction])
991 .observe(amount.sats_f64());
992 });
993}