Skip to main content

fedimint_lnv2_server/
lib.rs

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
79/// Maximum number of incoming contracts a single `await_incoming_contracts`
80/// request may ask for. The endpoint is public and unauthenticated, so the
81/// batch size is untrusted input; without a cap it flows straight into
82/// `Vec::with_capacity`, letting one request trigger an arbitrarily large
83/// allocation and abort the guardian process. The legitimate client requests
84/// 128 at a time, so this leaves ample headroom.
85const 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        // Eagerly register metrics so the series exist before the first transaction
246        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    // We evaluate the scalar polynomial of degree threshold - 1 at the point x
396    // using the Horner schema.
397
398    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        // We reduce the time granularity to deduplicate votes more often and not save
425        // one consensus item every second.
426        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        // Both incoming and outgoing contracts represent liabilities to the federation
646        // since they are obligations to issue notes.
647        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                    // Long-poll until the decryption key share exists. The share is written
701                    // atomically when the funding transaction output is accepted, so blocking
702                    // here lets a client request the share before acceptance and receive it the
703                    // instant it becomes available, rather than polling and backing off. This
704                    // mirrors the AWAIT_INCOMING_CONTRACT and AWAIT_PREIMAGE endpoints.
705                    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        // The block count we select guarantees that any threshold of correct peers can
793        // increase the consensus block count and any consensus block count has been
794        // confirmed by a threshold of peers.
795
796        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        // The unix time we select guarantees that any threshold of correct peers can
816        // advance the consensus unix time and any consensus unix time has been
817        // confirmed by a threshold of peers.
818
819        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            // to avoid race conditions we have to check for the contract and
837            // its expiration in the same database transaction
838            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            // to avoid race conditions we have to check for the preimage and
868            // the contracts expiration in the same database transaction
869            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        // Never pre-allocate from untrusted `n`: even though the endpoint bounds
910        // it, clamping here keeps the allocation safe for any caller. The `.take(n)`
911        // loop below grows the vec as needed anyway.
912        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}