Skip to main content

fedimint_ln_server/
lib.rs

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_api_client::api::{DynModuleApi, FederationApiExt};
14use fedimint_core::config::{
15    ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
16    TypedServerModuleConsensusConfig,
17};
18use fedimint_core::core::ModuleInstanceId;
19use fedimint_core::db::{DatabaseTransaction, DatabaseValue, IDatabaseTransactionOpsCoreTyped};
20use fedimint_core::encoding::Encodable;
21use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
22use fedimint_core::envs::{FM_ENABLE_MODULE_LNV1_ENV, is_env_var_set_opt, next_poll_delay};
23use fedimint_core::module::audit::Audit;
24use fedimint_core::module::{
25    Amounts, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion,
26    CoreConsensusVersion, InputMeta, ModuleConsensusVersion, ModuleInit, TransactionItemAmounts,
27    public_api_endpoint,
28};
29use fedimint_core::secp256k1::{Message, PublicKey, SECP256K1};
30use fedimint_core::task::{TaskGroup, sleep};
31use fedimint_core::util::{FmtCompact, FmtCompactAnyhow};
32use fedimint_core::{
33    Amount, InPoint, NumPeers, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send,
34    push_db_pair_items,
35};
36pub use fedimint_ln_common as common;
37use fedimint_ln_common::config::{
38    FeeConsensus, LightningClientConfig, LightningConfig, LightningConfigConsensus,
39    LightningConfigPrivate,
40};
41use fedimint_ln_common::contracts::incoming::{IncomingContractAccount, IncomingContractOffer};
42use fedimint_ln_common::contracts::{
43    Contract, ContractId, ContractOutcome, DecryptedPreimage, DecryptedPreimageStatus,
44    EncryptedPreimage, FundedContract, IdentifiableContract, Preimage, PreimageDecryptionShare,
45    PreimageKey,
46};
47use fedimint_ln_common::federation_endpoint_constants::{
48    ACCOUNT_ENDPOINT, AWAIT_ACCOUNT_ENDPOINT, AWAIT_BLOCK_HEIGHT_ENDPOINT, AWAIT_OFFER_ENDPOINT,
49    AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT, AWAIT_PREIMAGE_DECRYPTION, BLOCK_COUNT_ENDPOINT,
50    GET_DECRYPTED_PREIMAGE_STATUS, LIST_GATEWAYS_ENDPOINT, MODULE_CONSENSUS_VERSION_ENDPOINT,
51    OFFER_ENDPOINT, REGISTER_GATEWAY_ENDPOINT, REMOVE_GATEWAY_CHALLENGE_ENDPOINT,
52    REMOVE_GATEWAY_ENDPOINT, SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT,
53};
54use fedimint_ln_common::{
55    CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION, ContractAccount, LightningCommonInit,
56    LightningConsensusItem, LightningGatewayAnnouncement, LightningGatewayRegistration,
57    LightningInput, LightningInputError, LightningModuleTypes, LightningOutput,
58    LightningOutputError, LightningOutputOutcome, LightningOutputOutcomeV0, LightningOutputV0,
59    MODULE_CONSENSUS_VERSION, RemoveGatewayRequest, create_gateway_registration_message,
60    create_gateway_remove_message,
61};
62use fedimint_logging::LOG_MODULE_LN;
63use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
64use fedimint_server_core::config::PeerHandleOps;
65use fedimint_server_core::{
66    ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
67};
68use futures::StreamExt;
69use futures::future::join_all;
70use metrics::{LN_CANCEL_OUTGOING_CONTRACTS, LN_FUNDED_CONTRACT_SATS, LN_INCOMING_OFFER};
71use rand::rngs::OsRng;
72use strum::IntoEnumIterator;
73use threshold_crypto::poly::Commitment;
74use threshold_crypto::serde_impl::SerdeSecret;
75use threshold_crypto::{PublicKeySet, SecretKeyShare};
76use tokio::sync::watch;
77use tracing::{debug, error, info, info_span, trace, warn};
78
79use crate::db::{
80    AgreedDecryptionShareContractIdPrefix, AgreedDecryptionShareKey,
81    AgreedDecryptionShareKeyPrefix, BlockCountVoteKey, BlockCountVotePrefix,
82    ConsensusVersionVoteKey, ConsensusVersionVotePrefix, ContractKey, ContractKeyPrefix,
83    ContractUpdateKey, ContractUpdateKeyPrefix, DbKeyPrefix, EncryptedPreimageIndexKey,
84    EncryptedPreimageIndexKeyPrefix, LightningAuditItemKey, LightningAuditItemKeyPrefix,
85    LightningGatewayKey, LightningGatewayKeyPrefix, OfferKey, OfferKeyPrefix,
86    ProposeDecryptionShareKey, ProposeDecryptionShareKeyPrefix,
87};
88
89mod metrics;
90
91#[derive(Debug, Clone)]
92pub struct LightningInit;
93
94impl ModuleInit for LightningInit {
95    type Common = LightningCommonInit;
96
97    async fn dump_database(
98        &self,
99        dbtx: &mut DatabaseTransaction<'_>,
100        prefix_names: Vec<String>,
101    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
102        let mut lightning: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
103            BTreeMap::new();
104        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
105            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
106        });
107        for table in filtered_prefixes {
108            match table {
109                DbKeyPrefix::AgreedDecryptionShare => {
110                    push_db_pair_items!(
111                        dbtx,
112                        AgreedDecryptionShareKeyPrefix,
113                        AgreedDecryptionShareKey,
114                        PreimageDecryptionShare,
115                        lightning,
116                        "Accepted Decryption Shares"
117                    );
118                }
119                DbKeyPrefix::Contract => {
120                    push_db_pair_items!(
121                        dbtx,
122                        ContractKeyPrefix,
123                        ContractKey,
124                        ContractAccount,
125                        lightning,
126                        "Contracts"
127                    );
128                }
129                DbKeyPrefix::ContractUpdate => {
130                    push_db_pair_items!(
131                        dbtx,
132                        ContractUpdateKeyPrefix,
133                        ContractUpdateKey,
134                        LightningOutputOutcomeV0,
135                        lightning,
136                        "Contract Updates"
137                    );
138                }
139                DbKeyPrefix::LightningGateway => {
140                    push_db_pair_items!(
141                        dbtx,
142                        LightningGatewayKeyPrefix,
143                        LightningGatewayKey,
144                        LightningGatewayRegistration,
145                        lightning,
146                        "Lightning Gateways"
147                    );
148                }
149                DbKeyPrefix::Offer => {
150                    push_db_pair_items!(
151                        dbtx,
152                        OfferKeyPrefix,
153                        OfferKey,
154                        IncomingContractOffer,
155                        lightning,
156                        "Offers"
157                    );
158                }
159                DbKeyPrefix::ProposeDecryptionShare => {
160                    push_db_pair_items!(
161                        dbtx,
162                        ProposeDecryptionShareKeyPrefix,
163                        ProposeDecryptionShareKey,
164                        PreimageDecryptionShare,
165                        lightning,
166                        "Proposed Decryption Shares"
167                    );
168                }
169                DbKeyPrefix::BlockCountVote => {
170                    push_db_pair_items!(
171                        dbtx,
172                        BlockCountVotePrefix,
173                        BlockCountVoteKey,
174                        u64,
175                        lightning,
176                        "Block Count Votes"
177                    );
178                }
179                DbKeyPrefix::EncryptedPreimageIndex => {
180                    push_db_pair_items!(
181                        dbtx,
182                        EncryptedPreimageIndexKeyPrefix,
183                        EncryptedPreimageIndexKey,
184                        (),
185                        lightning,
186                        "Encrypted Preimage Hashes"
187                    );
188                }
189                DbKeyPrefix::LightningAuditItem => {
190                    push_db_pair_items!(
191                        dbtx,
192                        LightningAuditItemKeyPrefix,
193                        LightningAuditItemKey,
194                        Amount,
195                        lightning,
196                        "Lightning Audit Items"
197                    );
198                }
199                DbKeyPrefix::ConsensusVersionVote => {
200                    push_db_pair_items!(
201                        dbtx,
202                        ConsensusVersionVotePrefix,
203                        ConsensusVersionVoteKey,
204                        ModuleConsensusVersion,
205                        lightning,
206                        "Consensus Version Votes"
207                    );
208                }
209            }
210        }
211
212        Box::new(lightning.into_iter())
213    }
214}
215
216#[apply(async_trait_maybe_send!)]
217impl ServerModuleInit for LightningInit {
218    type Module = Lightning;
219
220    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
221        &[MODULE_CONSENSUS_VERSION]
222    }
223
224    fn is_enabled_by_default(&self) -> bool {
225        is_env_var_set_opt(FM_ENABLE_MODULE_LNV1_ENV).unwrap_or(false)
226    }
227
228    fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
229        vec![EnvVarDoc {
230            name: FM_ENABLE_MODULE_LNV1_ENV,
231            description: "Set to 1/true to enable the LNv1 Lightning module. Disabled by default.",
232        }]
233    }
234
235    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
236        // Eagerly initialize metrics that trigger infrequently
237        LN_CANCEL_OUTGOING_CONTRACTS.get();
238
239        let peer_supported_consensus_version =
240            Lightning::spawn_peer_supported_consensus_version_task(
241                args.module_api().clone(),
242                args.task_group(),
243                args.our_peer_id(),
244            );
245
246        Ok(Lightning {
247            cfg: args.cfg().to_typed()?,
248            our_peer_id: args.our_peer_id(),
249            num_peers: args.num_peers(),
250            peer_supported_consensus_version,
251            server_bitcoin_rpc_monitor: args.server_bitcoin_rpc_monitor(),
252        })
253    }
254
255    fn trusted_dealer_gen(
256        &self,
257        peers: &[PeerId],
258        args: &ConfigGenModuleArgs,
259    ) -> BTreeMap<PeerId, ServerModuleConfig> {
260        let sks = threshold_crypto::SecretKeySet::random(peers.to_num_peers().degree(), &mut OsRng);
261        let pks = sks.public_keys();
262
263        peers
264            .iter()
265            .map(|&peer| {
266                let sk = sks.secret_key_share(peer.to_usize());
267
268                (
269                    peer,
270                    LightningConfig {
271                        consensus: LightningConfigConsensus {
272                            threshold_pub_keys: pks.clone(),
273                            fee_consensus: FeeConsensus::default(),
274                            network: NetworkLegacyEncodingWrapper(args.network),
275                        },
276                        private: LightningConfigPrivate {
277                            threshold_sec_key: threshold_crypto::serde_impl::SerdeSecret(sk),
278                        },
279                    }
280                    .to_erased(),
281                )
282            })
283            .collect()
284    }
285
286    async fn distributed_gen(
287        &self,
288        peers: &(dyn PeerHandleOps + Send + Sync),
289        args: &ConfigGenModuleArgs,
290    ) -> anyhow::Result<ServerModuleConfig> {
291        let (polynomial, mut sks) = peers.run_dkg_g1().await?;
292
293        let server = LightningConfig {
294            consensus: LightningConfigConsensus {
295                threshold_pub_keys: PublicKeySet::from(Commitment::from(polynomial)),
296                fee_consensus: FeeConsensus::default(),
297                network: NetworkLegacyEncodingWrapper(args.network),
298            },
299            private: LightningConfigPrivate {
300                threshold_sec_key: SerdeSecret(SecretKeyShare::from_mut(&mut sks)),
301            },
302        };
303
304        Ok(server.to_erased())
305    }
306
307    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
308        let config = config.to_typed::<LightningConfig>()?;
309        if config.private.threshold_sec_key.public_key_share()
310            != config
311                .consensus
312                .threshold_pub_keys
313                .public_key_share(identity.to_usize())
314        {
315            bail!("Lightning private key doesn't match pubkey share");
316        }
317        Ok(())
318    }
319
320    fn get_client_config(
321        &self,
322        config: &ServerModuleConsensusConfig,
323    ) -> anyhow::Result<LightningClientConfig> {
324        let config = LightningConfigConsensus::from_erased(config)?;
325        Ok(LightningClientConfig {
326            threshold_pub_key: config.threshold_pub_keys.public_key(),
327            fee_consensus: config.fee_consensus,
328            network: config.network,
329        })
330    }
331
332    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
333        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
334    }
335}
336/// The lightning module implements an account system. It does not have the
337/// privacy guarantees of the e-cash mint module but instead allows for smart
338/// contracting. There exist two contract types that can be used to "lock"
339/// accounts:
340///
341///   * [Outgoing]: an account locked with an HTLC-like contract allowing to
342///     incentivize an external Lightning node to make payments for the funder
343///   * [Incoming]: a contract type that represents the acquisition of a
344///     preimage belonging to a hash. Every incoming contract is preceded by an
345///     offer that specifies how much the seller is asking for the preimage to a
346///     particular hash. It also contains some threshold-encrypted data. Once
347///     the contract is funded the data is decrypted. If it is a valid preimage
348///     the contract's funds are now accessible to the creator of the offer, if
349///     not they are accessible to the funder.
350///
351/// These two primitives allow to integrate the federation with the wider
352/// Lightning network through a centralized but untrusted (except for
353/// availability) Lightning gateway server.
354///
355/// [Outgoing]: fedimint_ln_common::contracts::outgoing::OutgoingContract
356/// [Incoming]: fedimint_ln_common::contracts::incoming::IncomingContract
357#[derive(Debug)]
358pub struct Lightning {
359    cfg: LightningConfig,
360    our_peer_id: PeerId,
361    num_peers: NumPeers,
362    /// The highest module consensus version supported by every peer, as
363    /// reported by their APIs, or `None` while any peer has yet to answer.
364    peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
365    server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
366}
367
368#[apply(async_trait_maybe_send!)]
369impl ServerModule for Lightning {
370    type Common = LightningModuleTypes;
371    type Init = LightningInit;
372
373    async fn consensus_proposal(
374        &self,
375        dbtx: &mut DatabaseTransaction<'_>,
376    ) -> Vec<LightningConsensusItem> {
377        let mut items: Vec<LightningConsensusItem> = dbtx
378            .find_by_prefix(&ProposeDecryptionShareKeyPrefix)
379            .await
380            .map(|(ProposeDecryptionShareKey(contract_id), share)| {
381                LightningConsensusItem::DecryptPreimage(contract_id, share)
382            })
383            .collect()
384            .await;
385
386        if let Ok(block_count_vote) = self.get_block_count() {
387            trace!(target: LOG_MODULE_LN, ?block_count_vote, "Proposing block count");
388            items.push(LightningConsensusItem::BlockCount(block_count_vote));
389        }
390
391        // Consensus upgrade activation voting. There is deliberately no manual
392        // override: a new consensus item variant is only understood by upgraded
393        // peers, and peers that predate it skip it rather than fail, which would
394        // silently fork their state. Requiring every peer to report support
395        // before we ever propose the item is what keeps that from happening.
396        let active_consensus_version = self.consensus_module_consensus_version(dbtx).await;
397
398        if let Some(supported_consensus_version) = *self.peer_supported_consensus_version.borrow()
399            // Only vote if the commonly supported version is higher than the
400            // currently active one
401            && active_consensus_version < supported_consensus_version
402        {
403            items.push(LightningConsensusItem::ModuleConsensusVersion(
404                supported_consensus_version,
405            ));
406        }
407
408        items
409    }
410
411    async fn process_consensus_item<'a, 'b>(
412        &'a self,
413        dbtx: &mut DatabaseTransaction<'b>,
414        consensus_item: LightningConsensusItem,
415        peer_id: PeerId,
416    ) -> anyhow::Result<()> {
417        let span = info_span!("process decryption share", %peer_id);
418        let _guard = span.enter();
419        trace!(target: LOG_MODULE_LN, ?consensus_item, "Processing consensus item proposal");
420
421        match consensus_item {
422            LightningConsensusItem::DecryptPreimage(contract_id, share) => {
423                if dbtx
424                    .get_value(&AgreedDecryptionShareKey(contract_id, peer_id))
425                    .await
426                    .is_some()
427                {
428                    bail!("Already received a valid decryption share for this peer");
429                }
430
431                let account = dbtx
432                    .get_value(&ContractKey(contract_id))
433                    .await
434                    .context("Contract account for this decryption share does not exist")?;
435
436                let (contract, out_point) = match account.contract {
437                    FundedContract::Incoming(contract) => (contract.contract, contract.out_point),
438                    FundedContract::Outgoing(..) => {
439                        bail!("Contract account for this decryption share is outgoing");
440                    }
441                };
442
443                if contract.decrypted_preimage != DecryptedPreimage::Pending {
444                    bail!("Contract for this decryption share is not pending");
445                }
446
447                if !self.validate_decryption_share(peer_id, &share, &contract.encrypted_preimage) {
448                    bail!("Decryption share is invalid");
449                }
450
451                // we save the first ordered valid decryption share for every peer
452                dbtx.insert_new_entry(&AgreedDecryptionShareKey(contract_id, peer_id), &share)
453                    .await;
454
455                // collect all valid decryption shares previously received for this contract
456                let decryption_shares = dbtx
457                    .find_by_prefix(&AgreedDecryptionShareContractIdPrefix(contract_id))
458                    .await
459                    .map(|(key, decryption_share)| (key.1, decryption_share))
460                    .collect::<Vec<_>>()
461                    .await;
462
463                if decryption_shares.len() < self.cfg.consensus.threshold() {
464                    return Ok(());
465                }
466
467                debug!(target: LOG_MODULE_LN, "Beginning to decrypt preimage");
468
469                let Ok(preimage_vec) = self.cfg.consensus.threshold_pub_keys.decrypt(
470                    decryption_shares
471                        .iter()
472                        .map(|(peer, share)| (peer.to_usize(), &share.0)),
473                    &contract.encrypted_preimage.0,
474                ) else {
475                    // TODO: check if that can happen even though shares are verified
476                    // before
477                    error!(target: LOG_MODULE_LN, contract_hash = %contract.hash, "Failed to decrypt preimage");
478                    return Ok(());
479                };
480
481                // Delete decryption shares once we've decrypted the preimage
482                dbtx.remove_entry(&ProposeDecryptionShareKey(contract_id))
483                    .await;
484
485                dbtx.remove_by_prefix(&AgreedDecryptionShareContractIdPrefix(contract_id))
486                    .await;
487
488                let decrypted_preimage = if preimage_vec.len() == 33
489                    && contract.hash
490                        == sha256::Hash::hash(&sha256::Hash::hash(&preimage_vec).to_byte_array())
491                {
492                    let preimage = PreimageKey(
493                        preimage_vec
494                            .as_slice()
495                            .try_into()
496                            .expect("Invalid preimage length"),
497                    );
498                    if preimage.to_public_key().is_ok() {
499                        DecryptedPreimage::Some(preimage)
500                    } else {
501                        DecryptedPreimage::Invalid
502                    }
503                } else {
504                    DecryptedPreimage::Invalid
505                };
506
507                debug!(target: LOG_MODULE_LN, ?decrypted_preimage);
508
509                // TODO: maybe define update helper fn
510                // Update contract
511                let contract_db_key = ContractKey(contract_id);
512                let mut contract_account = dbtx
513                    .get_value(&contract_db_key)
514                    .await
515                    .expect("checked before that it exists");
516                let incoming = match &mut contract_account.contract {
517                    FundedContract::Incoming(incoming) => incoming,
518                    FundedContract::Outgoing(_) => {
519                        unreachable!("previously checked that it's an incoming contract")
520                    }
521                };
522                incoming.contract.decrypted_preimage = decrypted_preimage.clone();
523                trace!(?contract_account, "Updating contract account");
524                dbtx.insert_entry(&contract_db_key, &contract_account).await;
525
526                // Update output outcome
527                let mut outcome = dbtx
528                    .get_value(&ContractUpdateKey(out_point))
529                    .await
530                    .expect("outcome was created on funding");
531
532                let LightningOutputOutcomeV0::Contract {
533                    outcome: ContractOutcome::Incoming(incoming_contract_outcome_preimage),
534                    ..
535                } = &mut outcome
536                else {
537                    panic!("We are expecting an incoming contract")
538                };
539                *incoming_contract_outcome_preimage = decrypted_preimage.clone();
540                dbtx.insert_entry(&ContractUpdateKey(out_point), &outcome)
541                    .await;
542            }
543            LightningConsensusItem::BlockCount(block_count) => {
544                let current_vote = dbtx
545                    .get_value(&BlockCountVoteKey(peer_id))
546                    .await
547                    .unwrap_or(0);
548
549                if block_count < current_vote {
550                    bail!("Block count vote decreased");
551                }
552
553                if block_count == current_vote {
554                    bail!("Block height vote is redundant");
555                }
556
557                dbtx.insert_entry(&BlockCountVoteKey(peer_id), &block_count)
558                    .await;
559            }
560            LightningConsensusItem::ModuleConsensusVersion(module_consensus_version) => {
561                let current_vote = dbtx
562                    .get_value(&ConsensusVersionVoteKey(peer_id))
563                    .await
564                    .unwrap_or(ModuleConsensusVersion::new(2, 0));
565
566                if module_consensus_version <= current_vote {
567                    bail!("Module consensus version vote is redundant");
568                }
569
570                dbtx.insert_entry(&ConsensusVersionVoteKey(peer_id), &module_consensus_version)
571                    .await;
572
573                assert!(
574                    self.consensus_module_consensus_version(dbtx).await <= MODULE_CONSENSUS_VERSION,
575                    "Lightning module does not support new consensus version, please upgrade the module"
576                );
577            }
578            LightningConsensusItem::Default { variant, .. } => {
579                bail!("Unknown lightning consensus item received, variant={variant}");
580            }
581        }
582
583        Ok(())
584    }
585
586    async fn process_input<'a, 'b, 'c>(
587        &'a self,
588        dbtx: &mut DatabaseTransaction<'c>,
589        input: &'b LightningInput,
590        _in_point: InPoint,
591    ) -> Result<InputMeta, LightningInputError> {
592        let input = input.ensure_v0_ref()?;
593
594        let mut account = dbtx
595            .get_value(&ContractKey(input.contract_id))
596            .await
597            .ok_or(LightningInputError::UnknownContract(input.contract_id))?;
598
599        if account.amount < input.amount {
600            return Err(LightningInputError::InsufficientFunds(
601                account.amount,
602                input.amount,
603            ));
604        }
605
606        let consensus_block_count = self.consensus_block_count(dbtx).await;
607
608        let pub_key = match &account.contract {
609            FundedContract::Outgoing(outgoing) => {
610                if u64::from(outgoing.timelock) + 1 > consensus_block_count && !outgoing.cancelled {
611                    // If the timelock hasn't expired yet …
612                    let preimage_hash = bitcoin_hashes::sha256::Hash::hash(
613                        &input
614                            .witness
615                            .as_ref()
616                            .ok_or(LightningInputError::MissingPreimage)?
617                            .0,
618                    );
619
620                    // … and the spender provides a valid preimage …
621                    if preimage_hash != outgoing.hash {
622                        return Err(LightningInputError::InvalidPreimage);
623                    }
624
625                    // … then the contract account can be spent using the gateway key,
626                    outgoing.gateway_key
627                } else {
628                    // otherwise the user can claim the funds back.
629                    outgoing.user_key
630                }
631            }
632            FundedContract::Incoming(incoming) => match &incoming.contract.decrypted_preimage {
633                // Once the preimage has been decrypted …
634                DecryptedPreimage::Pending => {
635                    return Err(LightningInputError::ContractNotReady);
636                }
637                // … either the user may spend the funds since they sold a valid preimage …
638                DecryptedPreimage::Some(preimage) => match preimage.to_public_key() {
639                    Ok(pub_key) => pub_key,
640                    Err(_) => return Err(LightningInputError::InvalidPreimage),
641                },
642                // … or the gateway may claim back funds for not receiving the advertised preimage.
643                DecryptedPreimage::Invalid => incoming.contract.gateway_key,
644            },
645        };
646
647        account.amount -= input.amount;
648
649        dbtx.insert_entry(&ContractKey(input.contract_id), &account)
650            .await;
651
652        // When a contract reaches a terminal state, the associated amount will be
653        // updated to 0. At this point, the contract no longer needs to be tracked
654        // for auditing liabilities, so we can safely remove the audit key.
655        let audit_key = LightningAuditItemKey::from_funded_contract(&account.contract);
656        if account.amount.msats == 0 {
657            dbtx.remove_entry(&audit_key).await;
658        } else {
659            dbtx.insert_entry(&audit_key, &account.amount).await;
660        }
661
662        Ok(InputMeta {
663            amount: TransactionItemAmounts {
664                amounts: Amounts::new_bitcoin(input.amount),
665                fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.contract_input),
666            },
667            pub_key,
668        })
669    }
670
671    async fn process_output<'a, 'b>(
672        &'a self,
673        dbtx: &mut DatabaseTransaction<'b>,
674        output: &'a LightningOutput,
675        out_point: OutPoint,
676    ) -> Result<TransactionItemAmounts, LightningOutputError> {
677        let output = output.ensure_v0_ref()?;
678
679        match output {
680            LightningOutputV0::Contract(contract) => {
681                // From consensus version 2.1 on, a contract account is funded
682                // exactly once. Contract ids do not commit to the full contract
683                // state, so before this version a second funding output for the
684                // same id topped up the existing account while keeping its
685                // state; for an incoming contract whose preimage decryption
686                // already reached a terminal state, the first contract's
687                // gateway or preimage holder could sweep the new funds. The
688                // pre-2.1 top-up path below must remain reachable so historic
689                // sessions replay identically.
690                if self.is_contract_funded_once_active(dbtx).await
691                    && dbtx
692                        .get_value(&ContractKey(contract.contract.contract_id()))
693                        .await
694                        .is_some()
695                {
696                    return Err(LightningOutputError::ContractAlreadyFunded(
697                        contract.contract.contract_id(),
698                    ));
699                }
700
701                // Incoming contracts are special, they need to match an offer
702                if let Contract::Incoming(incoming) = &contract.contract {
703                    // An incoming contract's id is only its payment hash, so a second
704                    // funding lands on the account created by the first one. While that
705                    // account is still waiting on a decryption proposal, funding it
706                    // again would overwrite the pending proposal, so reject it.
707                    if dbtx
708                        .get_value(&ProposeDecryptionShareKey(incoming.contract_id()))
709                        .await
710                        .is_some()
711                    {
712                        return Err(LightningOutputError::ContractAlreadyFunded(
713                            incoming.contract_id(),
714                        ));
715                    }
716
717                    let offer = dbtx
718                        .get_value(&OfferKey(incoming.hash))
719                        .await
720                        .ok_or(LightningOutputError::NoOffer(incoming.hash))?;
721
722                    if contract.amount < offer.amount {
723                        // If the account is not sufficiently funded fail the output
724                        return Err(LightningOutputError::InsufficientIncomingFunding(
725                            offer.amount,
726                            contract.amount,
727                        ));
728                    }
729
730                    // Nothing ties the funding contract's ciphertext to the one
731                    // the offer put up for sale, so a funder other than the
732                    // intended payer can consume an offer with a ciphertext of
733                    // their own. It decrypts to garbage, the `Invalid` arm hands
734                    // the funds back to their own `gateway_key`, and the account
735                    // they leave behind makes the payment hash unofferable and
736                    // unfundable for good.
737                    if self.is_contract_funded_once_active(dbtx).await
738                        && incoming.encrypted_preimage != offer.encrypted_preimage
739                    {
740                        return Err(LightningOutputError::EncryptedPreimageMismatch);
741                    }
742
743                    // A funder who names the decryption outcome themselves takes
744                    // the `Invalid` arm's refund to their own `gateway_key`, and
745                    // the share proposed for the contract is never consumed: the
746                    // contract is not pending, so the decryption share bails, and
747                    // the key it was proposed under is only removed on the path
748                    // that bail skips. `consensus_proposal` then re-emits an item
749                    // for it every second, on every guardian, for good.
750                    if self.is_contract_funded_once_active(dbtx).await
751                        && incoming.decrypted_preimage != DecryptedPreimage::Pending
752                    {
753                        return Err(LightningOutputError::PreDecryptedIncomingContract);
754                    }
755
756                    // The offer's ciphertext is verified when the offer is created, but the
757                    // contract carries its own copy, which consensus decoding only checks
758                    // for valid point encodings. `decrypt_share` below returns `None` for
759                    // exactly the ciphertexts that fail `verify()`.
760                    if !incoming.encrypted_preimage.0.verify() {
761                        return Err(LightningOutputError::InvalidEncryptedPreimage);
762                    }
763                }
764
765                if contract.amount == Amount::ZERO {
766                    return Err(LightningOutputError::ZeroOutput);
767                }
768
769                let contract_db_key = ContractKey(contract.contract.contract_id());
770
771                let updated_contract_account = dbtx.get_value(&contract_db_key).await.map_or_else(
772                    || ContractAccount {
773                        amount: contract.amount,
774                        contract: contract.contract.clone().to_funded(out_point),
775                    },
776                    |mut value: ContractAccount| {
777                        value.amount += contract.amount;
778                        value
779                    },
780                );
781
782                dbtx.insert_entry(
783                    &LightningAuditItemKey::from_funded_contract(
784                        &updated_contract_account.contract,
785                    ),
786                    &updated_contract_account.amount,
787                )
788                .await;
789
790                if dbtx
791                    .insert_entry(&contract_db_key, &updated_contract_account)
792                    .await
793                    .is_none()
794                {
795                    dbtx.on_commit(move || {
796                        record_funded_contract_metric(&updated_contract_account);
797                    });
798                }
799
800                dbtx.insert_new_entry(
801                    &ContractUpdateKey(out_point),
802                    &LightningOutputOutcomeV0::Contract {
803                        id: contract.contract.contract_id(),
804                        outcome: contract.contract.to_outcome(),
805                    },
806                )
807                .await;
808
809                if let Contract::Incoming(incoming) = &contract.contract {
810                    let offer = dbtx
811                        .get_value(&OfferKey(incoming.hash))
812                        .await
813                        .expect("offer exists if output is valid");
814
815                    let decryption_share = self
816                        .cfg
817                        .private
818                        .threshold_sec_key
819                        .decrypt_share(&incoming.encrypted_preimage.0)
820                        .ok_or(LightningOutputError::InvalidEncryptedPreimage)?;
821
822                    dbtx.insert_new_entry(
823                        &ProposeDecryptionShareKey(contract.contract.contract_id()),
824                        &PreimageDecryptionShare(decryption_share),
825                    )
826                    .await;
827
828                    dbtx.remove_entry(&OfferKey(offer.hash)).await;
829                }
830
831                Ok(TransactionItemAmounts {
832                    amounts: Amounts::new_bitcoin(contract.amount),
833                    fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.contract_output),
834                })
835            }
836            LightningOutputV0::Offer(offer) => {
837                // From consensus version 2.1 on, no offer can be created for a
838                // payment hash whose incoming contract account already exists:
839                // funding it could only top up that account (rejected above
840                // once 2.1 is active), so such an offer is a dead end that
841                // could still lure a gateway into accepting an HTLC it can
842                // never get funded for.
843                if self.is_contract_funded_once_active(dbtx).await
844                    && dbtx
845                        .get_value(&ContractKey(offer.contract_id()))
846                        .await
847                        .is_some()
848                {
849                    return Err(LightningOutputError::OfferForFundedContract(
850                        offer.contract_id(),
851                    ));
852                }
853
854                if !offer.encrypted_preimage.0.verify() {
855                    return Err(LightningOutputError::InvalidEncryptedPreimage);
856                }
857
858                // Check that each preimage is only offered for sale once, see #1397
859                if dbtx
860                    .insert_entry(
861                        &EncryptedPreimageIndexKey(offer.encrypted_preimage.consensus_hash()),
862                        &(),
863                    )
864                    .await
865                    .is_some()
866                {
867                    return Err(LightningOutputError::DuplicateEncryptedPreimage);
868                }
869
870                dbtx.insert_new_entry(
871                    &ContractUpdateKey(out_point),
872                    &LightningOutputOutcomeV0::Offer { id: offer.id() },
873                )
874                .await;
875
876                // TODO: sanity-check encrypted preimage size
877                if dbtx
878                    .insert_entry(&OfferKey(offer.hash), &(*offer).clone())
879                    .await
880                    .is_some()
881                {
882                    // Technically the error isn't due to a duplicate encrypted preimage but due to
883                    // a duplicate payment hash, practically it's the same problem though: re-using
884                    // the invoice key. Since we can't eaily extend the error enum we just re-use
885                    // this variant.
886                    return Err(LightningOutputError::DuplicateEncryptedPreimage);
887                }
888
889                dbtx.on_commit(|| {
890                    LN_INCOMING_OFFER.inc();
891                });
892
893                Ok(TransactionItemAmounts::ZERO)
894            }
895            LightningOutputV0::CancelOutgoing {
896                contract,
897                gateway_signature,
898            } => {
899                let contract_account = dbtx
900                    .get_value(&ContractKey(*contract))
901                    .await
902                    .ok_or(LightningOutputError::UnknownContract(*contract))?;
903
904                let outgoing_contract = match &contract_account.contract {
905                    FundedContract::Outgoing(contract) => contract,
906                    FundedContract::Incoming(_) => {
907                        return Err(LightningOutputError::NotOutgoingContract);
908                    }
909                };
910
911                SECP256K1
912                    .verify_schnorr(
913                        gateway_signature,
914                        &Message::from_digest(*outgoing_contract.cancellation_message().as_ref()),
915                        &outgoing_contract.gateway_key.x_only_public_key().0,
916                    )
917                    .map_err(|_| LightningOutputError::InvalidCancellationSignature)?;
918
919                let updated_contract_account = {
920                    let mut contract_account = dbtx
921                        .get_value(&ContractKey(*contract))
922                        .await
923                        .expect("Contract exists if output is valid");
924
925                    let outgoing_contract = match &mut contract_account.contract {
926                        FundedContract::Outgoing(contract) => contract,
927                        FundedContract::Incoming(_) => {
928                            panic!("Contract type was checked in validate_output");
929                        }
930                    };
931
932                    outgoing_contract.cancelled = true;
933
934                    contract_account
935                };
936
937                dbtx.insert_entry(&ContractKey(*contract), &updated_contract_account)
938                    .await;
939
940                dbtx.insert_new_entry(
941                    &ContractUpdateKey(out_point),
942                    &LightningOutputOutcomeV0::CancelOutgoingContract { id: *contract },
943                )
944                .await;
945
946                dbtx.on_commit(|| {
947                    LN_CANCEL_OUTGOING_CONTRACTS.inc();
948                });
949
950                Ok(TransactionItemAmounts::ZERO)
951            }
952        }
953    }
954
955    async fn output_status(
956        &self,
957        dbtx: &mut DatabaseTransaction<'_>,
958        out_point: OutPoint,
959    ) -> Option<LightningOutputOutcome> {
960        dbtx.get_value(&ContractUpdateKey(out_point))
961            .await
962            .map(LightningOutputOutcome::V0)
963    }
964
965    /// Reject funding a contract that already has an account, and creating an
966    /// offer for a payment hash whose incoming contract account already exists.
967    ///
968    /// Contract ids do not commit to the full contract state — an incoming
969    /// contract's id commits to the payment hash alone — so a second funding
970    /// output for the same id does not create a new account: it tops up the
971    /// existing one, which keeps the first contract's `decrypted_preimage`,
972    /// `encrypted_preimage` and `gateway_key`. If that state is already
973    /// terminal the new funds are immediately spendable by the *first*
974    /// contract's gateway, and no further decryption can take place.
975    ///
976    /// These are the same rules [`ServerModule::process_output`] enforces in
977    /// consensus from module consensus version 2.1 on. Enforcing them here as
978    /// well protects federations that have not activated 2.1 yet, with policy
979    /// strength only: it is only as strong as the weakest guardian and cannot
980    /// see intra-session ordering, so it does not cover two fundings submitted
981    /// in the same session.
982    #[doc(hidden)]
983    async fn verify_output_submission<'a, 'b>(
984        &'a self,
985        dbtx: &mut DatabaseTransaction<'b>,
986        output: &'a LightningOutput,
987        _out_point: OutPoint,
988    ) -> Result<(), LightningOutputError> {
989        match output.ensure_v0_ref()? {
990            LightningOutputV0::Contract(contract) => {
991                let contract_id = contract.contract.contract_id();
992
993                if dbtx.get_value(&ContractKey(contract_id)).await.is_some() {
994                    return Err(LightningOutputError::ContractAlreadyFunded(contract_id));
995                }
996            }
997            LightningOutputV0::Offer(offer) => {
998                if dbtx
999                    .get_value(&ContractKey(offer.contract_id()))
1000                    .await
1001                    .is_some()
1002                {
1003                    return Err(LightningOutputError::OfferForFundedContract(
1004                        offer.contract_id(),
1005                    ));
1006                }
1007            }
1008            LightningOutputV0::CancelOutgoing { .. } => {}
1009        }
1010
1011        Ok(())
1012    }
1013
1014    async fn audit(
1015        &self,
1016        dbtx: &mut DatabaseTransaction<'_>,
1017        audit: &mut Audit,
1018        module_instance_id: ModuleInstanceId,
1019    ) {
1020        audit
1021            .add_items(
1022                dbtx,
1023                module_instance_id,
1024                &LightningAuditItemKeyPrefix,
1025                // Both incoming and outgoing contracts represent liabilities to the federation
1026                // since they are obligations to issue notes.
1027                |_, v| -(v.msats as i64),
1028            )
1029            .await;
1030    }
1031
1032    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
1033        vec![
1034            public_api_endpoint! {
1035                BLOCK_COUNT_ENDPOINT,
1036                ApiVersion::new(0, 0),
1037                async |module: &Lightning, context, _v: ()| -> Option<u64> {
1038                    let db = context.db();
1039                    let mut dbtx = db.begin_transaction_nc().await;
1040                    Ok(Some(module.consensus_block_count(&mut dbtx).await))
1041                }
1042            },
1043            public_api_endpoint! {
1044                MODULE_CONSENSUS_VERSION_ENDPOINT,
1045                ApiVersion::new(0, 1),
1046                async |module: &Lightning, context, _params: ()| -> ModuleConsensusVersion {
1047                    let db = context.db();
1048                    let mut dbtx = db.begin_transaction_nc().await;
1049                    Ok(module.consensus_module_consensus_version(&mut dbtx).await)
1050                }
1051            },
1052            public_api_endpoint! {
1053                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT,
1054                ApiVersion::new(0, 1),
1055                async |_module: &Lightning, _context, _params: ()| -> ModuleConsensusVersion {
1056                    Ok(MODULE_CONSENSUS_VERSION)
1057                }
1058            },
1059            public_api_endpoint! {
1060                ACCOUNT_ENDPOINT,
1061                ApiVersion::new(0, 0),
1062                async |module: &Lightning, context, contract_id: ContractId| -> Option<ContractAccount> {
1063                    let db = context.db();
1064                    let mut dbtx = db.begin_transaction_nc().await;
1065                    Ok(module
1066                        .get_contract_account(&mut dbtx, contract_id)
1067                        .await)
1068                }
1069            },
1070            public_api_endpoint! {
1071                AWAIT_ACCOUNT_ENDPOINT,
1072                ApiVersion::new(0, 0),
1073                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
1074                    Ok(module
1075                        .wait_contract_account(context, contract_id)
1076                        .await)
1077                }
1078            },
1079            public_api_endpoint! {
1080                AWAIT_BLOCK_HEIGHT_ENDPOINT,
1081                ApiVersion::new(0, 0),
1082                async |module: &Lightning, context, block_height: u64| -> () {
1083                    let db = context.db();
1084                    let mut dbtx = db.begin_transaction_nc().await;
1085                    module.wait_block_height(block_height, &mut dbtx).await;
1086                    Ok(())
1087                }
1088            },
1089            public_api_endpoint! {
1090                AWAIT_OUTGOING_CONTRACT_CANCELLED_ENDPOINT,
1091                ApiVersion::new(0, 0),
1092                async |module: &Lightning, context, contract_id: ContractId| -> ContractAccount {
1093                    Ok(module.wait_outgoing_contract_account_cancelled(context, contract_id).await)
1094                }
1095            },
1096            public_api_endpoint! {
1097                GET_DECRYPTED_PREIMAGE_STATUS,
1098                ApiVersion::new(0, 0),
1099                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, DecryptedPreimageStatus) {
1100                    module.get_decrypted_preimage_status(context, contract_id).await
1101                }
1102            },
1103            public_api_endpoint! {
1104                AWAIT_PREIMAGE_DECRYPTION,
1105                ApiVersion::new(0, 0),
1106                async |module: &Lightning, context, contract_id: ContractId| -> (IncomingContractAccount, Option<Preimage>) {
1107                    Ok(module.wait_preimage_decrypted(context, contract_id).await)
1108                }
1109            },
1110            public_api_endpoint! {
1111                OFFER_ENDPOINT,
1112                ApiVersion::new(0, 0),
1113                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> Option<IncomingContractOffer> {
1114                    let db = context.db();
1115                    let mut dbtx = db.begin_transaction_nc().await;
1116                    Ok(module
1117                        .get_offer(&mut dbtx, payment_hash)
1118                        .await)
1119               }
1120            },
1121            public_api_endpoint! {
1122                AWAIT_OFFER_ENDPOINT,
1123                ApiVersion::new(0, 0),
1124                async |module: &Lightning, context, payment_hash: bitcoin_hashes::sha256::Hash| -> IncomingContractOffer {
1125                    Ok(module
1126                        .wait_offer(context, payment_hash)
1127                        .await)
1128                }
1129            },
1130            public_api_endpoint! {
1131                LIST_GATEWAYS_ENDPOINT,
1132                ApiVersion::new(0, 0),
1133                async |module: &Lightning, context, _v: ()| -> Vec<LightningGatewayAnnouncement> {
1134                    let db = context.db();
1135                    let mut dbtx = db.begin_transaction_nc().await;
1136                    Ok(module.list_gateways(&mut dbtx).await)
1137                }
1138            },
1139            public_api_endpoint! {
1140                REGISTER_GATEWAY_ENDPOINT,
1141                ApiVersion::new(0, 0),
1142                async |module: &Lightning, context, gateway: LightningGatewayAnnouncement| -> () {
1143                    let db = context.db();
1144                    let mut dbtx = db.begin_transaction().await;
1145                    let gateway_id = gateway.info.gateway_id;
1146                    module.register_gateway(&mut dbtx.to_ref_nc(), gateway).await.map_err(|err| {
1147                        warn!(target: LOG_MODULE_LN, err = %err.fmt_compact_anyhow(), %gateway_id, "Rejected gateway registration");
1148                        ApiError::bad_request(err.to_string())
1149                    })?;
1150                    dbtx.commit_tx_result().await?;
1151                    Ok(())
1152                }
1153            },
1154            public_api_endpoint! {
1155                REMOVE_GATEWAY_CHALLENGE_ENDPOINT,
1156                ApiVersion::new(0, 1),
1157                async |module: &Lightning, context, gateway_id: PublicKey| -> Option<sha256::Hash> {
1158                    let db = context.db();
1159                    let mut dbtx = db.begin_transaction_nc().await;
1160                    Ok(module.get_gateway_remove_challenge(gateway_id, &mut dbtx).await)
1161                }
1162            },
1163            public_api_endpoint! {
1164                REMOVE_GATEWAY_ENDPOINT,
1165                ApiVersion::new(0, 1),
1166                async |module: &Lightning, context, remove_gateway_request: RemoveGatewayRequest| -> bool {
1167                    let db = context.db();
1168                    let mut dbtx = db.begin_transaction().await;
1169                    let result = module.remove_gateway(remove_gateway_request.clone(), &mut dbtx.to_ref_nc()).await;
1170                    match result {
1171                        Ok(()) => {
1172                            dbtx.commit_tx_result().await?;
1173                            Ok(true)
1174                        },
1175                        Err(err) => {
1176                            warn!(target: LOG_MODULE_LN, err = %err.fmt_compact_anyhow(), gateway_id = %remove_gateway_request.gateway_id, "Unable to remove gateway registration");
1177                            Ok(false)
1178                        },
1179                    }
1180                }
1181            },
1182        ]
1183    }
1184}
1185
1186impl Lightning {
1187    fn get_block_count(&self) -> anyhow::Result<u64> {
1188        self.server_bitcoin_rpc_monitor
1189            .status()
1190            .map(|status| status.block_count)
1191            .context("Block count not available yet")
1192    }
1193
1194    async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1195        let peer_count = 3 * (self.cfg.consensus.threshold() / 2) + 1;
1196
1197        let mut counts = dbtx
1198            .find_by_prefix(&BlockCountVotePrefix)
1199            .await
1200            .map(|(.., count)| count)
1201            .collect::<Vec<_>>()
1202            .await;
1203
1204        assert!(counts.len() <= peer_count);
1205
1206        while counts.len() < peer_count {
1207            counts.push(0);
1208        }
1209
1210        counts.sort_unstable();
1211
1212        counts[peer_count / 2]
1213    }
1214
1215    async fn wait_block_height(&self, block_height: u64, dbtx: &mut DatabaseTransaction<'_>) {
1216        while block_height >= self.consensus_block_count(dbtx).await {
1217            sleep(Duration::from_secs(5)).await;
1218        }
1219    }
1220
1221    async fn consensus_module_consensus_version(
1222        &self,
1223        dbtx: &mut DatabaseTransaction<'_>,
1224    ) -> ModuleConsensusVersion {
1225        let mut versions = dbtx
1226            .find_by_prefix(&ConsensusVersionVotePrefix)
1227            .await
1228            .map(|entry| entry.1)
1229            .collect::<Vec<ModuleConsensusVersion>>()
1230            .await;
1231
1232        while versions.len() < self.num_peers.total() {
1233            versions.push(ModuleConsensusVersion::new(2, 0));
1234        }
1235
1236        assert_eq!(versions.len(), self.num_peers.total());
1237
1238        versions.sort_unstable();
1239
1240        assert!(versions.first() <= versions.last());
1241
1242        versions[self.num_peers.max_evil()]
1243    }
1244
1245    /// Whether the funded-exactly-once rules of
1246    /// [`CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION`] are active, i.e. the
1247    /// federation has voted that version in.
1248    async fn is_contract_funded_once_active(&self, dbtx: &mut DatabaseTransaction<'_>) -> bool {
1249        CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION
1250            <= self.consensus_module_consensus_version(dbtx).await
1251    }
1252
1253    /// Tracks the highest module consensus version supported by *every* peer.
1254    ///
1255    /// A vote is only ever proposed once this reports a version, which requires
1256    /// every peer to have answered. Peers that predate a version do not serve
1257    /// [`SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT`] at all, so they hold the
1258    /// federation back rather than being voted past.
1259    fn spawn_peer_supported_consensus_version_task(
1260        api_client: DynModuleApi,
1261        task_group: &TaskGroup,
1262        our_peer_id: PeerId,
1263    ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1264        let (sender, receiver) = watch::channel(None);
1265        task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1266            loop {
1267                let request_futures = api_client
1268                    .all_peers()
1269                    .iter()
1270                    .filter(|&&peer| peer != our_peer_id)
1271                    .map(|&peer| {
1272                        let api_client = api_client.clone();
1273
1274                        async move {
1275                            api_client
1276                                .request_single_peer::<ModuleConsensusVersion>(
1277                                    SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1278                                    ApiRequestErased::default(),
1279                                    peer,
1280                                )
1281                                .await
1282                                .inspect_err(|err| warn!(
1283                                    target: LOG_MODULE_LN,
1284                                    %peer,
1285                                    err = %err.fmt_compact(),
1286                                    "Failed to fetch supported consensus version from peer"
1287                                ))
1288                                .ok()
1289                        }
1290                    });
1291
1292                // A peer that does not answer runs a binary without version voting, so
1293                // collecting into an `Option` holds the federation back on any absence
1294                // rather than voting that peer past a version it cannot decode.
1295                let all_peers_supported_version = join_all(request_futures)
1296                    .await
1297                    .into_iter()
1298                    .collect::<Option<Vec<_>>>()
1299                    .map(|peer_versions| {
1300                        peer_versions
1301                            .into_iter()
1302                            .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
1303                            .min()
1304                            .expect("Our own version is always present")
1305                    });
1306
1307                debug!(
1308                    target: LOG_MODULE_LN,
1309                    ?all_peers_supported_version,
1310                    "Fetched supported consensus versions from peers"
1311                );
1312
1313                #[allow(clippy::disallowed_methods)]
1314                if sender.send(all_peers_supported_version).is_err() {
1315                    warn!(target: LOG_MODULE_LN, "Failed to send consensus version to watch channel, stopping task");
1316                    break;
1317                }
1318
1319                sleep(next_poll_delay(all_peers_supported_version.is_some())).await;
1320            }
1321        });
1322        receiver
1323    }
1324
1325    fn validate_decryption_share(
1326        &self,
1327        peer: PeerId,
1328        share: &PreimageDecryptionShare,
1329        message: &EncryptedPreimage,
1330    ) -> bool {
1331        self.cfg
1332            .consensus
1333            .threshold_pub_keys
1334            .public_key_share(peer.to_usize())
1335            .verify_decryption_share(&share.0, &message.0)
1336    }
1337
1338    async fn get_offer(
1339        &self,
1340        dbtx: &mut DatabaseTransaction<'_>,
1341        payment_hash: bitcoin_hashes::sha256::Hash,
1342    ) -> Option<IncomingContractOffer> {
1343        dbtx.get_value(&OfferKey(payment_hash)).await
1344    }
1345
1346    async fn wait_offer(
1347        &self,
1348        context: &mut ApiEndpointContext,
1349        payment_hash: bitcoin_hashes::sha256::Hash,
1350    ) -> IncomingContractOffer {
1351        let future = context.wait_key_exists(OfferKey(payment_hash));
1352        future.await
1353    }
1354
1355    async fn get_contract_account(
1356        &self,
1357        dbtx: &mut DatabaseTransaction<'_>,
1358        contract_id: ContractId,
1359    ) -> Option<ContractAccount> {
1360        dbtx.get_value(&ContractKey(contract_id)).await
1361    }
1362
1363    async fn wait_contract_account(
1364        &self,
1365        context: &mut ApiEndpointContext,
1366        contract_id: ContractId,
1367    ) -> ContractAccount {
1368        // not using a variable here leads to a !Send error
1369        let future = context.wait_key_exists(ContractKey(contract_id));
1370        future.await
1371    }
1372
1373    async fn wait_outgoing_contract_account_cancelled(
1374        &self,
1375        context: &mut ApiEndpointContext,
1376        contract_id: ContractId,
1377    ) -> ContractAccount {
1378        let future =
1379            context.wait_value_matches(ContractKey(contract_id), |contract| {
1380                match &contract.contract {
1381                    FundedContract::Outgoing(c) => c.cancelled,
1382                    FundedContract::Incoming(_) => false,
1383                }
1384            });
1385        future.await
1386    }
1387
1388    async fn get_decrypted_preimage_status(
1389        &self,
1390        context: &mut ApiEndpointContext,
1391        contract_id: ContractId,
1392    ) -> Result<(IncomingContractAccount, DecryptedPreimageStatus), ApiError> {
1393        let f_contract = context.wait_key_exists(ContractKey(contract_id));
1394        let contract = f_contract.await;
1395        // `ContractKey` holds either contract variant and anyone can fund an
1396        // outgoing contract, so the caller decides which variant we find here.
1397        let incoming_contract_account =
1398            Self::get_incoming_contract_account(contract).ok_or_else(|| {
1399                ApiError::bad_request("Contract is not an incoming contract".to_string())
1400            })?;
1401        Ok(
1402            match &incoming_contract_account.contract.decrypted_preimage {
1403                DecryptedPreimage::Some(key) => (
1404                    incoming_contract_account.clone(),
1405                    DecryptedPreimageStatus::Some(Preimage(
1406                        sha256::Hash::hash(&key.0).to_byte_array(),
1407                    )),
1408                ),
1409                DecryptedPreimage::Pending => {
1410                    (incoming_contract_account, DecryptedPreimageStatus::Pending)
1411                }
1412                DecryptedPreimage::Invalid => {
1413                    (incoming_contract_account, DecryptedPreimageStatus::Invalid)
1414                }
1415            },
1416        )
1417    }
1418
1419    async fn wait_preimage_decrypted(
1420        &self,
1421        context: &mut ApiEndpointContext,
1422        contract_id: ContractId,
1423    ) -> (IncomingContractAccount, Option<Preimage>) {
1424        let future =
1425            context.wait_value_matches(ContractKey(contract_id), |contract| {
1426                match &contract.contract {
1427                    FundedContract::Incoming(c) => match c.contract.decrypted_preimage {
1428                        DecryptedPreimage::Pending => false,
1429                        DecryptedPreimage::Some(_) | DecryptedPreimage::Invalid => true,
1430                    },
1431                    FundedContract::Outgoing(_) => false,
1432                }
1433            });
1434
1435        let decrypt_preimage = future.await;
1436        let incoming_contract_account = Self::get_incoming_contract_account(decrypt_preimage)
1437            .expect("the matcher above only resolves for incoming contracts");
1438        match incoming_contract_account
1439            .clone()
1440            .contract
1441            .decrypted_preimage
1442        {
1443            DecryptedPreimage::Some(key) => (
1444                incoming_contract_account,
1445                Some(Preimage(sha256::Hash::hash(&key.0).to_byte_array())),
1446            ),
1447            _ => (incoming_contract_account, None),
1448        }
1449    }
1450
1451    fn get_incoming_contract_account(contract: ContractAccount) -> Option<IncomingContractAccount> {
1452        match contract.contract {
1453            FundedContract::Incoming(incoming) => Some(IncomingContractAccount {
1454                amount: contract.amount,
1455                contract: incoming.contract,
1456            }),
1457            FundedContract::Outgoing(_) => None,
1458        }
1459    }
1460
1461    async fn list_gateways(
1462        &self,
1463        dbtx: &mut DatabaseTransaction<'_>,
1464    ) -> Vec<LightningGatewayAnnouncement> {
1465        let stream = dbtx.find_by_prefix(&LightningGatewayKeyPrefix).await;
1466        stream
1467            .filter_map(|(_, gw)| async { if gw.is_expired() { None } else { Some(gw) } })
1468            .collect::<Vec<LightningGatewayRegistration>>()
1469            .await
1470            .into_iter()
1471            .map(LightningGatewayRegistration::unanchor)
1472            .collect::<Vec<LightningGatewayAnnouncement>>()
1473    }
1474
1475    /// Stores a gateway registration, rejecting announcements that are not
1476    /// entitled to overwrite the record currently held for their `gateway_id`.
1477    ///
1478    /// A registration carrying a valid
1479    /// [`fedimint_ln_common::GatewayRegistrationAuth`] outranks an
1480    /// unsigned one. Since only the holder of the secret key behind
1481    /// `gateway_id` can produce a signature, this means:
1482    ///
1483    /// - a gateway that signs cannot have its record replaced by anyone else,
1484    /// - a gateway that does not sign is exactly as exposed as it was before
1485    ///   proofs existed, and no more,
1486    /// - an attacker can never lock a gateway out of its own `gateway_id`,
1487    ///   because unsigned records never block other unsigned registrations.
1488    ///
1489    /// So gateways gain protection individually as they upgrade, with no
1490    /// coordinated rollout and no regression for those that have not.
1491    async fn register_gateway(
1492        &self,
1493        dbtx: &mut DatabaseTransaction<'_>,
1494        mut gateway: LightningGatewayAnnouncement,
1495    ) -> anyhow::Result<()> {
1496        // Garbage collect expired gateways (since we're already writing to the DB)
1497        // Note: A "gotcha" of doing this here is that if two gateways are registered
1498        // at the same time, they will both attempt to delete the same expired gateways
1499        // and one of them will fail. This should be fine, since the other one will
1500        // succeed and the failed one will just try again.
1501        self.delete_expired_gateways(dbtx).await;
1502
1503        let gateway_id = gateway.info.gateway_id;
1504
1505        anyhow::ensure!(
1506            gateway.info.fees.proportional_millionths <= 1_000_000,
1507            "Gateway registration fee of {} proportional millionths exceeds the payment itself",
1508            gateway.info.fees.proportional_millionths
1509        );
1510
1511        // Reject a forged proof outright rather than silently downgrading it to an
1512        // unsigned registration, which would hide a misconfigured gateway.
1513        if let Some(auth) = &gateway.auth {
1514            let msg = create_gateway_registration_message(
1515                self.cfg.consensus.threshold_pub_keys.public_key(),
1516                auth.nonce,
1517                &gateway.info,
1518            );
1519
1520            auth.signature
1521                .verify(&msg, &gateway_id.x_only_public_key().0)
1522                .context("Invalid gateway registration signature")?;
1523        }
1524
1525        // Registrations are garbage collected above, so anything still present is
1526        // live and its claim on this `gateway_id` has to be honored.
1527        if let Some(existing) = dbtx.get_value(&LightningGatewayKey(gateway_id)).await
1528            && let Some(existing_auth) = existing.auth
1529        {
1530            let auth = gateway.auth.as_ref().context(
1531                "Gateway registration is signed and cannot be replaced by an unsigned one",
1532            )?;
1533
1534            // The nonce only has to move for announcements that actually change
1535            // something. Re-registering identical settings is the common case —
1536            // gateways refresh well inside the TTL — and replaying it cannot
1537            // achieve anything beyond extending a lifetime that is clamped
1538            // anyway. Exempting it keeps a gateway whose clock stepped backwards
1539            // from being locked out of refreshing its own registration.
1540            anyhow::ensure!(
1541                auth.nonce > existing_auth.nonce || gateway.info == existing.info,
1542                "Gateway registration nonce must increase to change settings, got {} but stored {}",
1543                auth.nonce,
1544                existing_auth.nonce
1545            );
1546
1547            // The exemption must not let the ratchet fall back, or it defeats
1548            // itself: gateways refresh every few minutes and `auth` is served
1549            // publicly, so an attacker could replay an old identical-settings
1550            // announcement to lower the stored nonce and then replay an
1551            // intermediate one to restore stale settings. Keep the highest nonce
1552            // seen, along with the signature that goes with it, so the stored
1553            // proof stays self-consistent for clients that verify it.
1554            if auth.nonce < existing_auth.nonce {
1555                gateway.auth = Some(existing_auth);
1556            }
1557        }
1558
1559        // Whether a gateway is vetted is the federation's judgement to make, not a
1560        // property a gateway gets to assert about itself.
1561        gateway.vetted = false;
1562
1563        dbtx.insert_entry(&LightningGatewayKey(gateway_id), &gateway.anchor())
1564            .await;
1565
1566        Ok(())
1567    }
1568
1569    async fn delete_expired_gateways(&self, dbtx: &mut DatabaseTransaction<'_>) {
1570        let expired_gateway_keys = dbtx
1571            .find_by_prefix(&LightningGatewayKeyPrefix)
1572            .await
1573            .filter_map(|(key, gw)| async move { if gw.is_expired() { Some(key) } else { None } })
1574            .collect::<Vec<LightningGatewayKey>>()
1575            .await;
1576
1577        for key in expired_gateway_keys {
1578            dbtx.remove_entry(&key).await;
1579        }
1580    }
1581
1582    /// Returns the challenge to the gateway that must be signed by the
1583    /// gateway's private key in order for the gateway registration record
1584    /// to be removed. The challenge is the concatenation of the gateway's
1585    /// public key and the `valid_until` bytes. This ensures that the
1586    /// challenges changes every time the gateway is re-registered and ensures
1587    /// that the challenge is unique per-gateway.
1588    async fn get_gateway_remove_challenge(
1589        &self,
1590        gateway_id: PublicKey,
1591        dbtx: &mut DatabaseTransaction<'_>,
1592    ) -> Option<sha256::Hash> {
1593        match dbtx.get_value(&LightningGatewayKey(gateway_id)).await {
1594            Some(gateway) => {
1595                let mut valid_until_bytes = vec![];
1596                fedimint_core::encoding::encode_legacy_system_time(
1597                    &gateway.valid_until,
1598                    &mut valid_until_bytes,
1599                )
1600                .expect("encoding to a vector cannot fail");
1601                let mut challenge_bytes = gateway_id.to_bytes();
1602                challenge_bytes.append(&mut valid_until_bytes);
1603                Some(sha256::Hash::hash(&challenge_bytes))
1604            }
1605            _ => None,
1606        }
1607    }
1608
1609    /// Removes the gateway registration record. First the signature provided by
1610    /// the gateway is verified by checking if the gateway's challenge has
1611    /// been signed by the gateway's private key.
1612    async fn remove_gateway(
1613        &self,
1614        remove_gateway_request: RemoveGatewayRequest,
1615        dbtx: &mut DatabaseTransaction<'_>,
1616    ) -> anyhow::Result<()> {
1617        let fed_public_key = self.cfg.consensus.threshold_pub_keys.public_key();
1618        let gateway_id = remove_gateway_request.gateway_id;
1619        let our_peer_id = self.our_peer_id;
1620        let signature = remove_gateway_request
1621            .signatures
1622            .get(&our_peer_id)
1623            .ok_or_else(|| {
1624                warn!(target: LOG_MODULE_LN, "No signature provided for gateway: {gateway_id}");
1625                anyhow::anyhow!("No signature provided for gateway {gateway_id}")
1626            })?;
1627
1628        // If there is no challenge, the gateway does not exist in the database and
1629        // there is nothing to do
1630        let challenge = self
1631            .get_gateway_remove_challenge(gateway_id, dbtx)
1632            .await
1633            .ok_or(anyhow::anyhow!(
1634                "Gateway {gateway_id} is not registered with peer {our_peer_id}"
1635            ))?;
1636
1637        // Verify the supplied schnorr signature is valid
1638        let msg = create_gateway_remove_message(fed_public_key, our_peer_id, challenge);
1639        signature.verify(&msg, &gateway_id.x_only_public_key().0)?;
1640
1641        dbtx.remove_entry(&LightningGatewayKey(gateway_id)).await;
1642        info!(target: LOG_MODULE_LN, "Successfully removed gateway: {gateway_id}");
1643        Ok(())
1644    }
1645}
1646
1647fn record_funded_contract_metric(updated_contract_account: &ContractAccount) {
1648    LN_FUNDED_CONTRACT_SATS
1649        .with_label_values(&[match updated_contract_account.contract {
1650            FundedContract::Incoming(_) => "incoming",
1651            FundedContract::Outgoing(_) => "outgoing",
1652        }])
1653        .observe(updated_contract_account.amount.sats_f64());
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658    use std::time::Duration;
1659
1660    use assert_matches::assert_matches;
1661    use bitcoin_hashes::{Hash as BitcoinHash, sha256};
1662    use fedimint_core::bitcoin::{Block, BlockHash};
1663    use fedimint_core::db::mem_impl::MemDatabase;
1664    use fedimint_core::db::{
1665        Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
1666    };
1667    use fedimint_core::encoding::{Decodable, Encodable};
1668    use fedimint_core::envs::BitcoinRpcConfig;
1669    use fedimint_core::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
1670    use fedimint_core::module::{Amounts, ApiEndpointContext, InputMeta, TransactionItemAmounts};
1671    use fedimint_core::secp256k1::{Keypair, PublicKey, SECP256K1, generate_keypair};
1672    use fedimint_core::task::TaskGroup;
1673    use fedimint_core::util::SafeUrl;
1674    use fedimint_core::{
1675        Amount, ChainId, Feerate, InPoint, NumPeers, OutPoint, PeerId, TransactionId,
1676    };
1677    use fedimint_ln_common::config::{LightningClientConfig, LightningConfig, Network};
1678    use fedimint_ln_common::contracts::incoming::{
1679        FundedIncomingContract, IncomingContract, IncomingContractOffer,
1680    };
1681    use fedimint_ln_common::contracts::outgoing::OutgoingContract;
1682    use fedimint_ln_common::contracts::{
1683        Contract, DecryptedPreimage, EncryptedPreimage, FundedContract, IdentifiableContract,
1684        Preimage, PreimageKey,
1685    };
1686    use fedimint_ln_common::{
1687        CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION, ContractAccount, ContractOutput,
1688        GatewayRegistrationAuth, LightningGateway, LightningGatewayAnnouncement, LightningInput,
1689        LightningOutput, LightningOutputError, MAX_GATEWAY_REGISTRATION_TTL,
1690    };
1691    use fedimint_server_core::bitcoin_rpc::{IServerBitcoinRpc, ServerBitcoinRpcMonitor};
1692    use fedimint_server_core::{ServerModule, ServerModuleInit};
1693    use rand::rngs::OsRng;
1694    use tokio::sync::watch;
1695
1696    use crate::db::{
1697        ConsensusVersionVoteKey, ContractKey, LightningAuditItemKey, LightningGatewayKey, OfferKey,
1698    };
1699    use crate::{Lightning, LightningInit, create_gateway_registration_message};
1700
1701    #[derive(Debug)]
1702    struct MockBitcoinServerRpc;
1703
1704    #[async_trait::async_trait]
1705    impl IServerBitcoinRpc for MockBitcoinServerRpc {
1706        fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
1707            BitcoinRpcConfig {
1708                kind: "mock".to_string(),
1709                url: "http://mock".parse().unwrap(),
1710            }
1711        }
1712
1713        fn get_url(&self) -> SafeUrl {
1714            "http://mock".parse().unwrap()
1715        }
1716
1717        async fn get_block_count(&self) -> anyhow::Result<u64> {
1718            Err(anyhow::anyhow!("Mock block count error"))
1719        }
1720
1721        async fn get_block_hash(&self, _height: u64) -> anyhow::Result<BlockHash> {
1722            Err(anyhow::anyhow!("Mock block hash error"))
1723        }
1724
1725        async fn get_block(&self, _block_hash: &BlockHash) -> anyhow::Result<Block> {
1726            Err(anyhow::anyhow!("Mock block error"))
1727        }
1728
1729        async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
1730            Err(anyhow::anyhow!("Mock feerate error"))
1731        }
1732
1733        async fn submit_transaction(
1734            &self,
1735            _transaction: fedimint_core::bitcoin::Transaction,
1736        ) -> anyhow::Result<()> {
1737            // No-op for mock
1738            Ok(())
1739        }
1740
1741        async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
1742            Err(anyhow::anyhow!("Mock sync percentage error"))
1743        }
1744
1745        async fn get_chain_id(&self) -> anyhow::Result<ChainId> {
1746            // Just mock something up
1747            Ok(ChainId(BlockHash::from_byte_array([1; 32])))
1748        }
1749    }
1750
1751    const MINTS: u16 = 4;
1752
1753    fn build_configs() -> (Vec<LightningConfig>, LightningClientConfig) {
1754        let peers = (0..MINTS).map(PeerId::from).collect::<Vec<_>>();
1755        let args = fedimint_server_core::ConfigGenModuleArgs {
1756            network: Network::Regtest,
1757            disable_base_fees: false,
1758        };
1759        let server_cfg = ServerModuleInit::trusted_dealer_gen(&LightningInit, &peers, &args);
1760
1761        let client_cfg = ServerModuleInit::get_client_config(
1762            &LightningInit,
1763            &server_cfg[&PeerId::from(0)].consensus,
1764        )
1765        .unwrap();
1766
1767        let server_cfg = server_cfg
1768            .into_values()
1769            .map(|config| {
1770                config
1771                    .to_typed()
1772                    .expect("Config was just generated by the same configgen")
1773            })
1774            .collect::<Vec<LightningConfig>>();
1775
1776        (server_cfg, client_cfg)
1777    }
1778
1779    fn random_pub_key() -> PublicKey {
1780        generate_keypair(&mut OsRng).1
1781    }
1782
1783    #[test_log::test(tokio::test)]
1784    async fn encrypted_preimage_only_usable_once() {
1785        let task_group = TaskGroup::new();
1786        let (server_cfg, client_cfg) = build_configs();
1787
1788        let server = mock_server(&server_cfg[0], &task_group);
1789
1790        let preimage = [42u8; 32];
1791        let encrypted_preimage = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([42; 32]));
1792
1793        let hash = preimage.consensus_hash();
1794        let offer = IncomingContractOffer {
1795            amount: Amount::from_sats(10),
1796            hash,
1797            encrypted_preimage: encrypted_preimage.clone(),
1798            expiry_time: None,
1799        };
1800        let output = LightningOutput::new_v0_offer(offer);
1801        let out_point = OutPoint {
1802            txid: TransactionId::all_zeros(),
1803            out_idx: 0,
1804        };
1805
1806        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1807        let mut dbtx = db.begin_transaction_nc().await;
1808
1809        server
1810            .process_output(
1811                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1812                &output,
1813                out_point,
1814            )
1815            .await
1816            .expect("First time works");
1817
1818        let hash2 = [21u8, 32].consensus_hash();
1819        let offer2 = IncomingContractOffer {
1820            amount: Amount::from_sats(1),
1821            hash: hash2,
1822            encrypted_preimage,
1823            expiry_time: None,
1824        };
1825        let output2 = LightningOutput::new_v0_offer(offer2);
1826        let out_point2 = OutPoint {
1827            txid: TransactionId::all_zeros(),
1828            out_idx: 1,
1829        };
1830
1831        assert_matches!(
1832            server
1833                .process_output(
1834                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1835                    &output2,
1836                    out_point2
1837                )
1838                .await,
1839            Err(_)
1840        );
1841    }
1842
1843    fn mock_server(cfg: &LightningConfig, task_group: &TaskGroup) -> Lightning {
1844        Lightning {
1845            cfg: cfg.clone(),
1846            our_peer_id: 0.into(),
1847            num_peers: NumPeers::from(usize::from(MINTS)),
1848            // No peer has reported a supported version, so no upgrade is ever
1849            // proposed. None of these tests exercise version voting.
1850            peer_supported_consensus_version: watch::channel(None).1,
1851            server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor::new(
1852                MockBitcoinServerRpc.into_dyn(),
1853                Duration::from_secs(1),
1854                task_group,
1855            ),
1856        }
1857    }
1858
1859    fn offer_output(hash: sha256::Hash, encrypted_preimage: &EncryptedPreimage) -> LightningOutput {
1860        LightningOutput::new_v0_offer(IncomingContractOffer {
1861            amount: Amount::from_msats(1),
1862            hash,
1863            encrypted_preimage: encrypted_preimage.clone(),
1864            expiry_time: None,
1865        })
1866    }
1867
1868    fn incoming_contract_output(
1869        hash: sha256::Hash,
1870        encrypted_preimage: &EncryptedPreimage,
1871    ) -> LightningOutput {
1872        LightningOutput::new_v0_contract(ContractOutput {
1873            amount: Amount::from_msats(1),
1874            contract: Contract::Incoming(IncomingContract {
1875                hash,
1876                encrypted_preimage: encrypted_preimage.clone(),
1877                decrypted_preimage: DecryptedPreimage::Pending,
1878                gateway_key: random_pub_key(),
1879            }),
1880        })
1881    }
1882
1883    /// Apply `outputs` as if they were the outputs of one transaction.
1884    async fn fund(
1885        server: &Lightning,
1886        dbtx: &mut fedimint_core::db::DatabaseTransaction<'_, Committable>,
1887        outputs: &[LightningOutput],
1888        txid_byte: u8,
1889    ) -> Result<(), LightningOutputError> {
1890        for (idx, output) in outputs.iter().enumerate() {
1891            server
1892                .process_output(
1893                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
1894                    output,
1895                    OutPoint {
1896                        txid: TransactionId::from_byte_array([txid_byte; 32]),
1897                        out_idx: idx as u64,
1898                    },
1899                )
1900                .await?;
1901        }
1902
1903        Ok(())
1904    }
1905
1906    /// Encryption is randomized and `EncryptedPreimage::verify` does not bind
1907    /// the ciphertext to the payment hash, so distinct ciphertexts for one
1908    /// hash are free to construct and the offer dedup index does not catch
1909    /// them.
1910    fn ciphertexts<const N: usize>(
1911        cfg: &LightningClientConfig,
1912        preimage: [u8; 32],
1913    ) -> [EncryptedPreimage; N] {
1914        std::array::from_fn(|_| EncryptedPreimage(cfg.threshold_pub_key.encrypt(preimage)))
1915    }
1916
1917    /// Funding a payment hash twice within one transaction: the first funding
1918    /// consumes the offer, so a second offer is accepted, and without the guard
1919    /// the second funding would overwrite the pending decryption proposal.
1920    #[test_log::test(tokio::test)]
1921    async fn incoming_funding_rejected_while_decryption_pending() {
1922        let task_group = TaskGroup::new();
1923        let (server_cfg, client_cfg) = build_configs();
1924        let server = mock_server(&server_cfg[0], &task_group);
1925
1926        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1927        let mut dbtx = db.begin_transaction().await;
1928
1929        let preimage = [42u8; 32];
1930        let hash = preimage.consensus_hash();
1931        let [ct1, ct2] = ciphertexts(&client_cfg, preimage);
1932
1933        fund(
1934            &server,
1935            &mut dbtx,
1936            &[
1937                offer_output(hash, &ct1),
1938                incoming_contract_output(hash, &ct1),
1939                offer_output(hash, &ct2),
1940            ],
1941            0x01,
1942        )
1943        .await
1944        .expect("the first offer/contract pair and the second offer are valid");
1945
1946        // A distinct txid, so the second funding reaches the pending-decryption
1947        // guard rather than colliding on the first transaction's
1948        // `ContractUpdateKey` outpoint and passing for the wrong reason.
1949        assert_matches!(
1950            fund(
1951                &server,
1952                &mut dbtx,
1953                &[incoming_contract_output(hash, &ct2)],
1954                0x02
1955            )
1956            .await,
1957            Err(LightningOutputError::ContractAlreadyFunded(_)),
1958            "funding again while the decryption proposal is pending must be rejected"
1959        );
1960    }
1961
1962    /// Two separately submitted transactions can both pass submission-mode
1963    /// validation while neither is committed yet, because the consensus data
1964    /// provider never re-validates. The guard therefore has to hold when
1965    /// consensus applies them in order, not just within one transaction.
1966    #[test_log::test(tokio::test)]
1967    async fn incoming_funding_rejected_across_transactions() {
1968        let task_group = TaskGroup::new();
1969        let (server_cfg, client_cfg) = build_configs();
1970        let server = mock_server(&server_cfg[0], &task_group);
1971
1972        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
1973
1974        let preimage = [42u8; 32];
1975        let hash = preimage.consensus_hash();
1976        let [ct1, ct2] = ciphertexts(&client_cfg, preimage);
1977
1978        let tx_a = [
1979            offer_output(hash, &ct1),
1980            incoming_contract_output(hash, &ct1),
1981        ];
1982        let tx_b = [
1983            offer_output(hash, &ct2),
1984            incoming_contract_output(hash, &ct2),
1985        ];
1986
1987        // both are validated against the same committed state, as neither has been
1988        // ordered yet, and both are queued for consensus
1989        for (outputs, txid_byte) in [(&tx_a, 0xaa), (&tx_b, 0xbb)] {
1990            let mut dbtx = db.begin_transaction().await;
1991            dbtx.ignore_uncommitted();
1992            fund(&server, &mut dbtx, outputs, txid_byte)
1993                .await
1994                .expect("both transactions are valid against the pre-funding state");
1995        }
1996
1997        let mut dbtx = db.begin_transaction().await;
1998        fund(&server, &mut dbtx, &tx_a, 0xaa)
1999            .await
2000            .expect("the first transaction is accepted");
2001        dbtx.commit_tx().await;
2002
2003        let mut dbtx = db.begin_transaction().await;
2004        assert_matches!(
2005            fund(&server, &mut dbtx, &tx_b, 0xbb).await,
2006            Err(LightningOutputError::ContractAlreadyFunded(_)),
2007            "the second transaction must be rejected instead of panicking"
2008        );
2009    }
2010
2011    /// Every funding is gated on an offer and consumes it, so a payment hash
2012    /// can never be funded twice without a fresh offer being created in
2013    /// between. This bounds how fast an attacker can reach a second
2014    /// funding, and is what makes the funder-side check in the client
2015    /// race-free.
2016    #[test_log::test(tokio::test)]
2017    async fn incoming_offer_is_consumed_by_funding() {
2018        let task_group = TaskGroup::new();
2019        let (server_cfg, client_cfg) = build_configs();
2020        let server = mock_server(&server_cfg[0], &task_group);
2021
2022        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2023
2024        let preimage = [42u8; 32];
2025        let hash = preimage.consensus_hash();
2026        let [ct1] = ciphertexts(&client_cfg, preimage);
2027
2028        let mut dbtx = db.begin_transaction().await;
2029        fund(
2030            &server,
2031            &mut dbtx,
2032            &[
2033                offer_output(hash, &ct1),
2034                incoming_contract_output(hash, &ct1),
2035            ],
2036            0x01,
2037        )
2038        .await
2039        .expect("offer and funding accepted");
2040
2041        assert!(
2042            dbtx.to_ref_with_prefix_module_id(42)
2043                .0
2044                .into_nc()
2045                .get_value(&OfferKey(hash))
2046                .await
2047                .is_none(),
2048            "a funding must consume the offer that gated it"
2049        );
2050    }
2051
2052    /// The tightest race available to an attacker: poison the account and
2053    /// re-arm an offer in a single transaction, so that a funder's
2054    /// transaction ordered immediately afterwards still finds an offer. The
2055    /// poisoned contract is necessarily still pending at that point, so the
2056    /// guard catches it.
2057    #[test_log::test(tokio::test)]
2058    async fn poisoning_and_rearming_an_offer_leaves_the_contract_pending() {
2059        let task_group = TaskGroup::new();
2060        let (server_cfg, client_cfg) = build_configs();
2061        let server = mock_server(&server_cfg[0], &task_group);
2062
2063        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2064
2065        let preimage = [42u8; 32];
2066        let hash = preimage.consensus_hash();
2067        let [ct1, ct2, ct3] = ciphertexts(&client_cfg, preimage);
2068
2069        let mut dbtx = db.begin_transaction().await;
2070        fund(&server, &mut dbtx, &[offer_output(hash, &ct1)], 0x01)
2071            .await
2072            .expect("offer accepted");
2073        dbtx.commit_tx().await;
2074
2075        let mut dbtx = db.begin_transaction().await;
2076        fund(
2077            &server,
2078            &mut dbtx,
2079            &[
2080                incoming_contract_output(hash, &ct1),
2081                offer_output(hash, &ct2),
2082            ],
2083            0x02,
2084        )
2085        .await
2086        .expect("poisoning funding and re-armed offer accepted");
2087        dbtx.commit_tx().await;
2088
2089        let mut dbtx = db.begin_transaction().await;
2090        assert_matches!(
2091            fund(
2092                &server,
2093                &mut dbtx,
2094                &[incoming_contract_output(hash, &ct3)],
2095                0x03
2096            )
2097            .await,
2098            Err(LightningOutputError::ContractAlreadyFunded(_)),
2099            "a funder ordered right after the re-arm must not top up the poisoned account"
2100        );
2101    }
2102
2103    /// An incoming contract carries its own ciphertext, which used to reach
2104    /// `decrypt_share` unvalidated and panic the guardian mid-consensus.
2105    #[test_log::test(tokio::test)]
2106    async fn incoming_contract_with_unverifiable_ciphertext_is_rejected() {
2107        let task_group = TaskGroup::new();
2108        let (server_cfg, client_cfg) = build_configs();
2109
2110        let server = mock_server(&server_cfg[0], &task_group);
2111
2112        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2113        let mut dbtx = db.begin_transaction_nc().await;
2114
2115        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2116        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2117        let valid_preimage = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2118
2119        server
2120            .process_output(
2121                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2122                &LightningOutput::new_v0_offer(IncomingContractOffer {
2123                    amount: Amount::from_sats(10),
2124                    hash,
2125                    encrypted_preimage: valid_preimage.clone(),
2126                    expiry_time: None,
2127                }),
2128                OutPoint {
2129                    txid: TransactionId::all_zeros(),
2130                    out_idx: 0,
2131                },
2132            )
2133            .await
2134            .expect("offer with a valid ciphertext is accepted");
2135
2136        // Mutate the ciphertext and round-trip it through the consensus
2137        // encoding, so the poisoned value is one a peer could receive on the
2138        // wire rather than one only constructible in-process.
2139        let encoded = valid_preimage.consensus_encode_to_vec();
2140        let unverifiable = (0..encoded.len())
2141            .flat_map(|pos| (0..8u8).map(move |bit| (pos, bit)))
2142            .find_map(|(pos, bit)| {
2143                let mut mutated = encoded.clone();
2144                mutated[pos] ^= 1 << bit;
2145                EncryptedPreimage::consensus_decode_whole(
2146                    &mutated,
2147                    &ModuleDecoderRegistry::default(),
2148                )
2149                .ok()
2150                .filter(|decoded| !decoded.0.verify())
2151            })
2152            .expect("a decodable but unverifiable ciphertext exists");
2153
2154        let output = LightningOutput::new_v0_contract(ContractOutput {
2155            amount: Amount::from_sats(10),
2156            contract: Contract::Incoming(IncomingContract {
2157                hash,
2158                encrypted_preimage: unverifiable,
2159                decrypted_preimage: DecryptedPreimage::Pending,
2160                gateway_key: random_pub_key(),
2161            }),
2162        });
2163
2164        assert_matches!(
2165            server
2166                .process_output(
2167                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2168                    &output,
2169                    OutPoint {
2170                        txid: TransactionId::all_zeros(),
2171                        out_idx: 1,
2172                    },
2173                )
2174                .await,
2175            Err(LightningOutputError::InvalidEncryptedPreimage)
2176        );
2177    }
2178
2179    /// Funding an incoming contract that already has an account is rejected at
2180    /// submission time, so the stale-state top-up from the security report
2181    /// cannot be reached through the transaction submission API.
2182    #[test_log::test(tokio::test)]
2183    async fn submission_rejects_refunding_an_incoming_contract() {
2184        let task_group = TaskGroup::new();
2185        let (server_cfg, client_cfg) = build_configs();
2186        let server = mock_server(&server_cfg[0], &task_group);
2187
2188        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2189        let mut dbtx = db.begin_transaction().await;
2190
2191        let op = |i: u64| OutPoint {
2192            txid: TransactionId::all_zeros(),
2193            out_idx: i,
2194        };
2195
2196        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2197        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2198        let ct_1 = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([0xAAu8; 33]));
2199        let ct_2 = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2200
2201        server
2202            .process_output(
2203                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2204                &LightningOutput::new_v0_offer(IncomingContractOffer {
2205                    amount: Amount::from_msats(1),
2206                    hash,
2207                    encrypted_preimage: ct_1.clone(),
2208                    expiry_time: None,
2209                }),
2210                op(0),
2211            )
2212            .await
2213            .expect("offer #1 accepted");
2214
2215        let first_contract = Contract::Incoming(IncomingContract {
2216            hash,
2217            encrypted_preimage: ct_1,
2218            decrypted_preimage: DecryptedPreimage::Pending,
2219            gateway_key: random_pub_key(),
2220        });
2221        let contract_id = first_contract.contract_id();
2222
2223        server
2224            .process_output(
2225                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2226                &LightningOutput::new_v0_contract(ContractOutput {
2227                    amount: Amount::from_msats(1),
2228                    contract: first_contract,
2229                }),
2230                op(1),
2231            )
2232            .await
2233            .expect("contract #1 funded");
2234
2235        // pre-2.1 consensus still accepts a second offer for the same payment
2236        // hash, but the submission policy rejects it
2237        let second_offer = LightningOutput::new_v0_offer(IncomingContractOffer {
2238            amount: Amount::from_msats(100_000),
2239            hash,
2240            encrypted_preimage: ct_2.clone(),
2241            expiry_time: None,
2242        });
2243
2244        assert_eq!(
2245            server
2246                .verify_output_submission(
2247                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2248                    &second_offer,
2249                    op(2),
2250                )
2251                .await,
2252            Err(LightningOutputError::OfferForFundedContract(contract_id))
2253        );
2254
2255        server
2256            .process_output(
2257                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2258                &second_offer,
2259                op(2),
2260            )
2261            .await
2262            .expect("offer #2 accepted");
2263
2264        let second_funding = LightningOutput::new_v0_contract(ContractOutput {
2265            amount: Amount::from_msats(100_000),
2266            contract: Contract::Incoming(IncomingContract {
2267                hash,
2268                encrypted_preimage: ct_2,
2269                decrypted_preimage: DecryptedPreimage::Pending,
2270                gateway_key: random_pub_key(),
2271            }),
2272        });
2273
2274        assert_eq!(
2275            server
2276                .verify_output_submission(
2277                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2278                    &second_funding,
2279                    op(3),
2280                )
2281                .await,
2282            Err(LightningOutputError::ContractAlreadyFunded(contract_id))
2283        );
2284    }
2285
2286    /// A first funding of any contract passes the submission check; a second
2287    /// funding of the same contract id is rejected for outgoing contracts as
2288    /// well, since their id does not commit to the amount either.
2289    #[test_log::test(tokio::test)]
2290    async fn submission_allows_only_first_fundings() {
2291        let task_group = TaskGroup::new();
2292        let (server_cfg, client_cfg) = build_configs();
2293        let server = mock_server(&server_cfg[0], &task_group);
2294
2295        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2296        let mut dbtx = db.begin_transaction().await;
2297
2298        let op = |i: u64| OutPoint {
2299            txid: TransactionId::all_zeros(),
2300            out_idx: i,
2301        };
2302
2303        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2304        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2305        let encrypted_preimage =
2306            EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2307
2308        let first_funding = LightningOutput::new_v0_contract(ContractOutput {
2309            amount: Amount::from_msats(100_000),
2310            contract: Contract::Incoming(IncomingContract {
2311                hash,
2312                encrypted_preimage: encrypted_preimage.clone(),
2313                decrypted_preimage: DecryptedPreimage::Pending,
2314                gateway_key: random_pub_key(),
2315            }),
2316        });
2317
2318        server
2319            .verify_output_submission(
2320                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2321                &first_funding,
2322                op(1),
2323            )
2324            .await
2325            .expect("first funding of an incoming contract is allowed");
2326
2327        let outgoing_contract = OutgoingContract {
2328            hash,
2329            gateway_key: random_pub_key(),
2330            timelock: 1_000_000,
2331            user_key: random_pub_key(),
2332            cancelled: false,
2333        };
2334        let outgoing = LightningOutput::new_v0_contract(ContractOutput {
2335            amount: Amount::from_msats(1000),
2336            contract: Contract::Outgoing(outgoing_contract.clone()),
2337        });
2338
2339        server
2340            .verify_output_submission(
2341                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2342                &outgoing,
2343                op(2),
2344            )
2345            .await
2346            .expect("first funding of an outgoing contract is allowed");
2347
2348        server
2349            .process_output(
2350                &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2351                &outgoing,
2352                op(2),
2353            )
2354            .await
2355            .expect("outgoing contract funded");
2356
2357        assert_eq!(
2358            server
2359                .verify_output_submission(
2360                    &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
2361                    &outgoing,
2362                    op(3),
2363                )
2364                .await,
2365            Err(LightningOutputError::ContractAlreadyFunded(
2366                outgoing_contract.contract_id()
2367            ))
2368        );
2369    }
2370
2371    /// Once 2.1 is active, a funder cannot name the decryption outcome. Without
2372    /// the check the funding is accepted, the funder reclaims via the `Invalid`
2373    /// arm, and the `ProposeDecryptionShareKey` it leaves is re-emitted as a
2374    /// consensus item once per second per guardian for good.
2375    #[test_log::test(tokio::test)]
2376    async fn consensus_v21_rejects_a_contract_funded_with_a_decided_preimage() {
2377        let task_group = TaskGroup::new();
2378        let (server_cfg, client_cfg) = build_configs();
2379        let server = mock_server(&server_cfg[0], &task_group);
2380
2381        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2382        let mut dbtx = db.begin_transaction().await;
2383        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0.into_nc();
2384
2385        for peer in 0..3u16 {
2386            module_dbtx
2387                .insert_new_entry(
2388                    &ConsensusVersionVoteKey(PeerId::from(peer)),
2389                    &CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION,
2390                )
2391                .await;
2392        }
2393
2394        let op = |i: u64| OutPoint {
2395            txid: TransactionId::all_zeros(),
2396            out_idx: i,
2397        };
2398
2399        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2400        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2401        let ciphertext = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2402
2403        server
2404            .process_output(
2405                &mut module_dbtx.to_ref_nc(),
2406                &LightningOutput::new_v0_offer(IncomingContractOffer {
2407                    amount: Amount::from_msats(1),
2408                    hash,
2409                    encrypted_preimage: ciphertext.clone(),
2410                    expiry_time: None,
2411                }),
2412                op(0),
2413            )
2414            .await
2415            .expect("offer accepted");
2416
2417        let fund = |decrypted_preimage: DecryptedPreimage| {
2418            LightningOutput::new_v0_contract(ContractOutput {
2419                amount: Amount::from_msats(1),
2420                contract: Contract::Incoming(IncomingContract {
2421                    hash,
2422                    encrypted_preimage: ciphertext.clone(),
2423                    decrypted_preimage,
2424                    gateway_key: random_pub_key(),
2425                }),
2426            })
2427        };
2428
2429        for decided in [
2430            DecryptedPreimage::Invalid,
2431            DecryptedPreimage::Some(preimage.clone()),
2432        ] {
2433            assert_eq!(
2434                server
2435                    .process_output(&mut module_dbtx.to_ref_nc(), &fund(decided), op(1))
2436                    .await
2437                    .expect_err("a funder must not name the decryption outcome"),
2438                LightningOutputError::PreDecryptedIncomingContract
2439            );
2440        }
2441
2442        server
2443            .process_output(
2444                &mut module_dbtx.to_ref_nc(),
2445                &fund(DecryptedPreimage::Pending),
2446                op(2),
2447            )
2448            .await
2449            .expect("a pending funding is still accepted");
2450    }
2451
2452    /// Once 2.1 is active, a funder cannot consume an offer with a ciphertext
2453    /// of their own. Without the binding the funding is accepted, decrypts to
2454    /// garbage, refunds to the funder's own `gateway_key`, and leaves an
2455    /// account that makes the payment hash permanently unofferable.
2456    #[test_log::test(tokio::test)]
2457    async fn consensus_v21_rejects_a_contract_whose_ciphertext_is_not_the_offers() {
2458        let task_group = TaskGroup::new();
2459        let (server_cfg, client_cfg) = build_configs();
2460        let server = mock_server(&server_cfg[0], &task_group);
2461
2462        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2463        let mut dbtx = db.begin_transaction().await;
2464        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0.into_nc();
2465
2466        for peer in 0..3u16 {
2467            module_dbtx
2468                .insert_new_entry(
2469                    &ConsensusVersionVoteKey(PeerId::from(peer)),
2470                    &CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION,
2471                )
2472                .await;
2473        }
2474
2475        let op = |i: u64| OutPoint {
2476            txid: TransactionId::all_zeros(),
2477            out_idx: i,
2478        };
2479
2480        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2481        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2482        let offered = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2483        let attackers = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([0xAAu8; 33]));
2484
2485        server
2486            .process_output(
2487                &mut module_dbtx.to_ref_nc(),
2488                &LightningOutput::new_v0_offer(IncomingContractOffer {
2489                    amount: Amount::from_msats(1),
2490                    hash,
2491                    encrypted_preimage: offered.clone(),
2492                    expiry_time: None,
2493                }),
2494                op(0),
2495            )
2496            .await
2497            .expect("offer accepted");
2498
2499        let fund = |ciphertext: EncryptedPreimage| {
2500            LightningOutput::new_v0_contract(ContractOutput {
2501                amount: Amount::from_msats(1),
2502                contract: Contract::Incoming(IncomingContract {
2503                    hash,
2504                    encrypted_preimage: ciphertext,
2505                    decrypted_preimage: DecryptedPreimage::Pending,
2506                    gateway_key: random_pub_key(),
2507                }),
2508            })
2509        };
2510
2511        assert_eq!(
2512            server
2513                .process_output(&mut module_dbtx.to_ref_nc(), &fund(attackers), op(1))
2514                .await
2515                .expect_err("a ciphertext that is not the offer's must be rejected"),
2516            LightningOutputError::EncryptedPreimageMismatch
2517        );
2518
2519        server
2520            .process_output(&mut module_dbtx.to_ref_nc(), &fund(offered), op(2))
2521            .await
2522            .expect("the offer's own ciphertext still funds it");
2523    }
2524
2525    /// Once the federation has voted in consensus version 2.1, re-funding an
2526    /// existing contract and creating an offer for an already funded incoming
2527    /// contract are rejected by consensus itself, not just submission policy.
2528    ///
2529    /// The incoming account is seeded in the stale terminal state from the
2530    /// security report (`DecryptedPreimage::Invalid`, no pending decryption
2531    /// proposal), which the pre-2.1 consensus rules would happily top up.
2532    #[test_log::test(tokio::test)]
2533    async fn consensus_v21_rejects_refunding_and_offers_for_funded_contracts() {
2534        let task_group = TaskGroup::new();
2535        let (server_cfg, client_cfg) = build_configs();
2536        let server = mock_server(&server_cfg[0], &task_group);
2537
2538        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2539        let mut dbtx = db.begin_transaction().await;
2540        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0.into_nc();
2541
2542        // Three of four peers vote for 2.1, so the max_evil()-th lowest vote
2543        // (index 1 of the sorted votes, with the fourth peer padded to 2.0)
2544        // reaches 2.1 and activates the funded-exactly-once rules.
2545        for peer in 0..3u16 {
2546            module_dbtx
2547                .insert_new_entry(
2548                    &ConsensusVersionVoteKey(PeerId::from(peer)),
2549                    &CONTRACT_FUNDED_ONCE_MODULE_CONSENSUS_VERSION,
2550                )
2551                .await;
2552        }
2553
2554        let op = |i: u64| OutPoint {
2555            txid: TransactionId::all_zeros(),
2556            out_idx: i,
2557        };
2558
2559        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2560        let hash = sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array());
2561        let ct_1 = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt([0xAAu8; 33]));
2562        let ct_2 = EncryptedPreimage(client_cfg.threshold_pub_key.encrypt(preimage.0));
2563
2564        let stale_contract = IncomingContract {
2565            hash,
2566            encrypted_preimage: ct_1,
2567            decrypted_preimage: DecryptedPreimage::Invalid,
2568            gateway_key: random_pub_key(),
2569        };
2570        let contract_id = stale_contract.contract_id();
2571
2572        module_dbtx
2573            .insert_new_entry(
2574                &ContractKey(contract_id),
2575                &ContractAccount {
2576                    amount: Amount::from_msats(1),
2577                    contract: FundedContract::Incoming(FundedIncomingContract {
2578                        contract: stale_contract,
2579                        out_point: op(0),
2580                    }),
2581                },
2582            )
2583            .await;
2584
2585        // an offer for the funded contract's payment hash is rejected
2586        assert_eq!(
2587            server
2588                .process_output(
2589                    &mut module_dbtx.to_ref_nc(),
2590                    &LightningOutput::new_v0_offer(IncomingContractOffer {
2591                        amount: Amount::from_msats(100_000),
2592                        hash,
2593                        encrypted_preimage: ct_2.clone(),
2594                        expiry_time: None,
2595                    }),
2596                    op(1),
2597                )
2598                .await
2599                .expect_err("offer for a funded contract must be rejected"),
2600            LightningOutputError::OfferForFundedContract(contract_id)
2601        );
2602
2603        // and so is a second funding of the same contract id
2604        assert_eq!(
2605            server
2606                .process_output(
2607                    &mut module_dbtx.to_ref_nc(),
2608                    &LightningOutput::new_v0_contract(ContractOutput {
2609                        amount: Amount::from_msats(100_000),
2610                        contract: Contract::Incoming(IncomingContract {
2611                            hash,
2612                            encrypted_preimage: ct_2,
2613                            decrypted_preimage: DecryptedPreimage::Pending,
2614                            gateway_key: random_pub_key(),
2615                        }),
2616                    }),
2617                    op(2),
2618                )
2619                .await
2620                .expect_err("re-funding a funded contract must be rejected"),
2621            LightningOutputError::ContractAlreadyFunded(contract_id)
2622        );
2623
2624        // outgoing contracts: the first funding passes, a second one is
2625        // rejected as well
2626        let outgoing_contract = OutgoingContract {
2627            hash,
2628            gateway_key: random_pub_key(),
2629            timelock: 1_000_000,
2630            user_key: random_pub_key(),
2631            cancelled: false,
2632        };
2633        let outgoing = LightningOutput::new_v0_contract(ContractOutput {
2634            amount: Amount::from_msats(1000),
2635            contract: Contract::Outgoing(outgoing_contract.clone()),
2636        });
2637
2638        server
2639            .process_output(&mut module_dbtx.to_ref_nc(), &outgoing, op(3))
2640            .await
2641            .expect("first funding of an outgoing contract is accepted");
2642
2643        assert_eq!(
2644            server
2645                .process_output(&mut module_dbtx.to_ref_nc(), &outgoing, op(4))
2646                .await
2647                .expect_err("re-funding an outgoing contract must be rejected"),
2648            LightningOutputError::ContractAlreadyFunded(outgoing_contract.contract_id())
2649        );
2650    }
2651
2652    #[test_log::test(tokio::test)]
2653    async fn process_input_for_valid_incoming_contracts() {
2654        let task_group = TaskGroup::new();
2655        let (server_cfg, client_cfg) = build_configs();
2656        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2657        let mut dbtx = db.begin_transaction_nc().await;
2658        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
2659
2660        let server = mock_server(&server_cfg[0], &task_group);
2661
2662        let preimage = PreimageKey(generate_keypair(&mut OsRng).1.serialize());
2663        let funded_incoming_contract = FundedContract::Incoming(FundedIncomingContract {
2664            contract: IncomingContract {
2665                hash: sha256::Hash::hash(&sha256::Hash::hash(&preimage.0).to_byte_array()),
2666                encrypted_preimage: EncryptedPreimage(
2667                    client_cfg.threshold_pub_key.encrypt(preimage.0),
2668                ),
2669                decrypted_preimage: DecryptedPreimage::Some(preimage.clone()),
2670                gateway_key: random_pub_key(),
2671            },
2672            out_point: OutPoint {
2673                txid: TransactionId::all_zeros(),
2674                out_idx: 0,
2675            },
2676        });
2677
2678        let contract_id = funded_incoming_contract.contract_id();
2679        let audit_key = LightningAuditItemKey::from_funded_contract(&funded_incoming_contract);
2680        let amount = Amount { msats: 1000 };
2681        let lightning_input = LightningInput::new_v0(contract_id, amount, None);
2682
2683        module_dbtx.insert_new_entry(&audit_key, &amount).await;
2684        module_dbtx
2685            .insert_new_entry(
2686                &ContractKey(contract_id),
2687                &ContractAccount {
2688                    amount,
2689                    contract: funded_incoming_contract,
2690                },
2691            )
2692            .await;
2693
2694        let processed_input_meta = server
2695            .process_input(
2696                &mut module_dbtx.to_ref_nc(),
2697                &lightning_input,
2698                InPoint {
2699                    txid: TransactionId::all_zeros(),
2700                    in_idx: 0,
2701                },
2702            )
2703            .await
2704            .expect("should process valid incoming contract");
2705        let expected_input_meta = InputMeta {
2706            amount: TransactionItemAmounts {
2707                amounts: Amounts::new_bitcoin(amount),
2708                fees: Amounts::ZERO,
2709            },
2710            pub_key: preimage
2711                .to_public_key()
2712                .expect("should create Schnorr pubkey from preimage"),
2713        };
2714
2715        assert_eq!(processed_input_meta, expected_input_meta);
2716
2717        let audit_item = module_dbtx.get_value(&audit_key).await;
2718        assert_eq!(audit_item, None);
2719    }
2720
2721    #[test_log::test(tokio::test)]
2722    async fn process_input_for_valid_outgoing_contracts() {
2723        let task_group = TaskGroup::new();
2724        let (server_cfg, _) = build_configs();
2725        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2726        let mut dbtx = db.begin_transaction_nc().await;
2727        let mut module_dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
2728
2729        let server = mock_server(&server_cfg[0], &task_group);
2730
2731        let preimage = Preimage([42u8; 32]);
2732        let gateway_key = random_pub_key();
2733        let outgoing_contract = FundedContract::Outgoing(OutgoingContract {
2734            hash: preimage.consensus_hash(),
2735            gateway_key,
2736            timelock: 1_000_000,
2737            user_key: random_pub_key(),
2738            cancelled: false,
2739        });
2740        let contract_id = outgoing_contract.contract_id();
2741        let audit_key = LightningAuditItemKey::from_funded_contract(&outgoing_contract);
2742        let amount = Amount { msats: 1000 };
2743        let lightning_input = LightningInput::new_v0(contract_id, amount, Some(preimage.clone()));
2744
2745        module_dbtx.insert_new_entry(&audit_key, &amount).await;
2746        module_dbtx
2747            .insert_new_entry(
2748                &ContractKey(contract_id),
2749                &ContractAccount {
2750                    amount,
2751                    contract: outgoing_contract,
2752                },
2753            )
2754            .await;
2755
2756        let processed_input_meta = server
2757            .process_input(
2758                &mut module_dbtx.to_ref_nc(),
2759                &lightning_input,
2760                InPoint {
2761                    txid: TransactionId::all_zeros(),
2762                    in_idx: 0,
2763                },
2764            )
2765            .await
2766            .expect("should process valid outgoing contract");
2767
2768        let expected_input_meta = InputMeta {
2769            amount: TransactionItemAmounts {
2770                amounts: Amounts::new_bitcoin(amount),
2771                fees: Amounts::ZERO,
2772            },
2773            pub_key: gateway_key,
2774        };
2775
2776        assert_eq!(processed_input_meta, expected_input_meta);
2777
2778        let audit_item = module_dbtx.get_value(&audit_key).await;
2779        assert_eq!(audit_item, None);
2780    }
2781
2782    /// `GET_DECRYPTED_PREIMAGE_STATUS` is unauthenticated and looks the
2783    /// contract up by `ContractKey`, which holds either contract variant.
2784    /// Anyone can fund an outgoing contract for a single msat and then point
2785    /// the endpoint at it, so a non-incoming contract has to be answered with
2786    /// an error rather than a panic.
2787    #[test_log::test(tokio::test)]
2788    async fn decrypted_preimage_status_rejects_an_outgoing_contract() {
2789        let (server, db, _task_group) = build_server();
2790
2791        let outgoing_contract = FundedContract::Outgoing(OutgoingContract {
2792            hash: Preimage([42u8; 32]).consensus_hash(),
2793            gateway_key: random_pub_key(),
2794            timelock: 1_000_000,
2795            user_key: random_pub_key(),
2796            cancelled: false,
2797        });
2798        let contract_id = outgoing_contract.contract_id();
2799
2800        let mut dbtx = db.begin_transaction().await;
2801        dbtx.insert_new_entry(
2802            &ContractKey(contract_id),
2803            &ContractAccount {
2804                amount: Amount { msats: 1 },
2805                contract: outgoing_contract,
2806            },
2807        )
2808        .await;
2809        dbtx.commit_tx().await;
2810
2811        let mut context = ApiEndpointContext::new(db, false);
2812        let error = server
2813            .get_decrypted_preimage_status(&mut context, contract_id)
2814            .await
2815            .expect_err("an outgoing contract has no decrypted preimage status");
2816
2817        assert_eq!(error.code, 400);
2818    }
2819
2820    /// Builds a `Lightning` server module backed by an in-memory database.
2821    fn build_server() -> (Lightning, Database, TaskGroup) {
2822        let task_group = TaskGroup::new();
2823        let (server_cfg, _) = build_configs();
2824        let server = mock_server(&server_cfg[0], &task_group);
2825        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
2826        (server, db, task_group)
2827    }
2828
2829    fn announcement(gateway_id: PublicKey, api: &str) -> LightningGatewayAnnouncement {
2830        LightningGatewayAnnouncement {
2831            info: LightningGateway {
2832                federation_index: 1,
2833                gateway_redeem_key: random_pub_key(),
2834                node_pub_key: random_pub_key(),
2835                lightning_alias: "gw".to_string(),
2836                api: api.parse().expect("valid url"),
2837                route_hints: vec![],
2838                fees: fedimint_ln_common::lightning_invoice::RoutingFees {
2839                    base_msat: 1000,
2840                    proportional_millionths: 100,
2841                },
2842                gateway_id,
2843                supports_private_payments: true,
2844            },
2845            vetted: false,
2846            ttl: Duration::from_secs(600),
2847            auth: None,
2848        }
2849    }
2850
2851    /// Signs `announcement` the way an upgraded gateway would.
2852    fn sign(
2853        server: &Lightning,
2854        keypair: &Keypair,
2855        nonce: u64,
2856        mut announcement: LightningGatewayAnnouncement,
2857    ) -> LightningGatewayAnnouncement {
2858        let msg = create_gateway_registration_message(
2859            server.cfg.consensus.threshold_pub_keys.public_key(),
2860            nonce,
2861            &announcement.info,
2862        );
2863        announcement.auth = Some(GatewayRegistrationAuth {
2864            nonce,
2865            signature: keypair.sign_schnorr(msg),
2866        });
2867        announcement
2868    }
2869
2870    async fn stored_api(
2871        server: &Lightning,
2872        dbtx: &mut DatabaseTransaction<'_>,
2873        id: PublicKey,
2874    ) -> String {
2875        server
2876            .list_gateways(dbtx)
2877            .await
2878            .into_iter()
2879            .find(|gw| gw.info.gateway_id == id)
2880            .expect("gateway is registered")
2881            .info
2882            .api
2883            .to_string()
2884    }
2885
2886    /// The core of the fix: once a gateway proves it holds the key behind its
2887    /// `gateway_id`, nobody else can take that identity over.
2888    #[test_log::test(tokio::test)]
2889    async fn signed_registration_cannot_be_overwritten_by_unsigned_one() {
2890        let (server, db, _tg) = build_server();
2891        let mut dbtx = db.begin_transaction().await;
2892        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
2893
2894        let victim = Keypair::new(SECP256K1, &mut OsRng);
2895        let gateway_id = victim.public_key();
2896
2897        let honest = sign(
2898            &server,
2899            &victim,
2900            1,
2901            announcement(gateway_id, "https://honest.gw/v1"),
2902        );
2903        server
2904            .register_gateway(&mut dbtx.to_ref_nc(), honest)
2905            .await
2906            .expect("honest gateway registers");
2907
2908        // Same `gateway_id`, attacker-chosen everything else, but no proof.
2909        let hijack = announcement(gateway_id, "https://attacker.example/v1");
2910        let err = server
2911            .register_gateway(&mut dbtx.to_ref_nc(), hijack)
2912            .await
2913            .expect_err("unsigned announcement must not replace a signed one");
2914        assert!(
2915            err.to_string()
2916                .contains("cannot be replaced by an unsigned one")
2917        );
2918
2919        assert_eq!(
2920            stored_api(&server, &mut dbtx.to_ref_nc(), gateway_id).await,
2921            "https://honest.gw/v1"
2922        );
2923    }
2924
2925    /// The upgrade path: a gateway that starts signing takes back its own
2926    /// `gateway_id`, even if someone squatted it first.
2927    #[test_log::test(tokio::test)]
2928    async fn signed_registration_overwrites_unsigned_squat() {
2929        let (server, db, _tg) = build_server();
2930        let mut dbtx = db.begin_transaction().await;
2931        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
2932
2933        let victim = Keypair::new(SECP256K1, &mut OsRng);
2934        let gateway_id = victim.public_key();
2935
2936        // Attacker gets there first, while the gateway is still un-upgraded.
2937        server
2938            .register_gateway(
2939                &mut dbtx.to_ref_nc(),
2940                announcement(gateway_id, "https://attacker.example/v1"),
2941            )
2942            .await
2943            .expect("unsigned squat is accepted, as it is today");
2944
2945        let honest = sign(
2946            &server,
2947            &victim,
2948            1,
2949            announcement(gateway_id, "https://honest.gw/v1"),
2950        );
2951        server
2952            .register_gateway(&mut dbtx.to_ref_nc(), honest)
2953            .await
2954            .expect("signed registration reclaims the identity");
2955
2956        assert_eq!(
2957            stored_api(&server, &mut dbtx.to_ref_nc(), gateway_id).await,
2958            "https://honest.gw/v1"
2959        );
2960    }
2961
2962    /// Backwards compatibility: gateways that have not upgraded keep working
2963    /// exactly as they do today. They are no better protected, but crucially no
2964    /// worse off, and an attacker cannot use the new rule to lock them out of
2965    /// their own `gateway_id`.
2966    #[test_log::test(tokio::test)]
2967    async fn unsigned_registrations_still_overwrite_each_other() {
2968        let (server, db, _tg) = build_server();
2969        let mut dbtx = db.begin_transaction().await;
2970        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
2971
2972        let gateway_id = random_pub_key();
2973
2974        // An attacker squats the identity of a legacy gateway before it first
2975        // registers. The legacy gateway must still be able to register.
2976        server
2977            .register_gateway(
2978                &mut dbtx.to_ref_nc(),
2979                announcement(gateway_id, "https://attacker.example/v1"),
2980            )
2981            .await
2982            .expect("first unsigned registration accepted");
2983        server
2984            .register_gateway(
2985                &mut dbtx.to_ref_nc(),
2986                announcement(gateway_id, "https://legacy.gw/v1"),
2987            )
2988            .await
2989            .expect("legacy gateway must not be locked out of its own identity");
2990
2991        assert_eq!(
2992            stored_api(&server, &mut dbtx.to_ref_nc(), gateway_id).await,
2993            "https://legacy.gw/v1"
2994        );
2995    }
2996
2997    #[test_log::test(tokio::test)]
2998    async fn registration_with_forged_signature_is_rejected() {
2999        let (server, db, _tg) = build_server();
3000        let mut dbtx = db.begin_transaction().await;
3001        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3002
3003        let victim = Keypair::new(SECP256K1, &mut OsRng);
3004        let attacker = Keypair::new(SECP256K1, &mut OsRng);
3005        let gateway_id = victim.public_key();
3006
3007        // Attacker signs the victim's `gateway_id` with their own key.
3008        let forged = sign(
3009            &server,
3010            &attacker,
3011            1,
3012            announcement(gateway_id, "https://attacker.example/v1"),
3013        );
3014        let err = server
3015            .register_gateway(&mut dbtx.to_ref_nc(), forged)
3016            .await
3017            .expect_err("signature by the wrong key must be rejected");
3018        assert!(
3019            err.to_string()
3020                .contains("Invalid gateway registration signature")
3021        );
3022
3023        assert!(server.list_gateways(&mut dbtx.to_ref_nc()).await.is_empty());
3024    }
3025
3026    /// A rate above one million cannot be represented by the deployed `LNv1`
3027    /// fee formula, and used to panic clients that priced it. Refuse to
3028    /// store one.
3029    #[test_log::test(tokio::test)]
3030    async fn registration_with_absurd_proportional_fee_is_rejected() {
3031        let (server, db, _tg) = build_server();
3032        let mut dbtx = db.begin_transaction().await;
3033        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3034
3035        let gateway_id = random_pub_key();
3036        let mut absurd = announcement(gateway_id, "https://gw.example/v1");
3037        absurd.info.fees.proportional_millionths = 1_000_001;
3038
3039        let err = server
3040            .register_gateway(&mut dbtx.to_ref_nc(), absurd)
3041            .await
3042            .expect_err("a fee larger than the payment must be rejected");
3043        assert!(err.to_string().contains("exceeds the payment itself"));
3044        assert!(server.list_gateways(&mut dbtx.to_ref_nc()).await.is_empty());
3045
3046        let mut at_limit = announcement(gateway_id, "https://gw.example/v1");
3047        at_limit.info.fees.proportional_millionths = 1_000_000;
3048
3049        server
3050            .register_gateway(&mut dbtx.to_ref_nc(), at_limit)
3051            .await
3052            .expect("a fee equal to the payment is within the bound");
3053        assert_eq!(server.list_gateways(&mut dbtx.to_ref_nc()).await.len(), 1);
3054    }
3055
3056    /// A captured proof must not be replayable to roll a gateway back to
3057    /// settings it has since moved off.
3058    #[test_log::test(tokio::test)]
3059    async fn replayed_registration_is_rejected() {
3060        let (server, db, _tg) = build_server();
3061        let mut dbtx = db.begin_transaction().await;
3062        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3063
3064        let victim = Keypair::new(SECP256K1, &mut OsRng);
3065        let gateway_id = victim.public_key();
3066
3067        let stale = sign(
3068            &server,
3069            &victim,
3070            1,
3071            announcement(gateway_id, "https://old.gw/v1"),
3072        );
3073        let current = sign(
3074            &server,
3075            &victim,
3076            2,
3077            announcement(gateway_id, "https://new.gw/v1"),
3078        );
3079
3080        server
3081            .register_gateway(&mut dbtx.to_ref_nc(), stale.clone())
3082            .await
3083            .expect("first registration accepted");
3084        server
3085            .register_gateway(&mut dbtx.to_ref_nc(), current)
3086            .await
3087            .expect("newer registration accepted");
3088
3089        let err = server
3090            .register_gateway(&mut dbtx.to_ref_nc(), stale)
3091            .await
3092            .expect_err("replay of the older signed announcement must be rejected");
3093        assert!(err.to_string().contains("nonce must increase"));
3094
3095        assert_eq!(
3096            stored_api(&server, &mut dbtx.to_ref_nc(), gateway_id).await,
3097            "https://new.gw/v1"
3098        );
3099    }
3100
3101    /// A proof is bound to one federation, so it cannot be lifted from a
3102    /// federation the attacker runs and replayed at the victim's.
3103    #[test_log::test(tokio::test)]
3104    async fn registration_proof_is_federation_bound() {
3105        let (server, db, _tg) = build_server();
3106        let (other_server, _other_db, _tg2) = build_server();
3107        let mut dbtx = db.begin_transaction().await;
3108        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3109
3110        let victim = Keypair::new(SECP256K1, &mut OsRng);
3111        let gateway_id = victim.public_key();
3112
3113        assert_ne!(
3114            server.cfg.consensus.threshold_pub_keys.public_key(),
3115            other_server.cfg.consensus.threshold_pub_keys.public_key(),
3116            "test federations must differ for this to prove anything"
3117        );
3118
3119        // Validly signed, but for a different federation.
3120        let foreign = sign(
3121            &other_server,
3122            &victim,
3123            1,
3124            announcement(gateway_id, "https://gw/v1"),
3125        );
3126        server
3127            .register_gateway(&mut dbtx.to_ref_nc(), foreign)
3128            .await
3129            .expect_err("proof from another federation must not verify here");
3130    }
3131
3132    /// `vetted` is the federation's judgement, not something a registrant may
3133    /// assert about itself.
3134    #[test_log::test(tokio::test)]
3135    async fn self_asserted_vetted_flag_is_cleared() {
3136        let (server, db, _tg) = build_server();
3137        let mut dbtx = db.begin_transaction().await;
3138        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3139
3140        let gateway_id = random_pub_key();
3141        let mut ann = announcement(gateway_id, "https://attacker.example/v1");
3142        ann.vetted = true;
3143
3144        server
3145            .register_gateway(&mut dbtx.to_ref_nc(), ann)
3146            .await
3147            .expect("registration accepted");
3148
3149        let stored = dbtx
3150            .to_ref_nc()
3151            .get_value(&LightningGatewayKey(gateway_id))
3152            .await
3153            .expect("registration exists");
3154        assert!(
3155            !stored.vetted,
3156            "guardian must not store a self-asserted vetted flag"
3157        );
3158    }
3159
3160    /// An unbounded TTL previously let a record outlive any expiry sweep, and a
3161    /// large enough one overflowed `SystemTime` outright.
3162    #[test_log::test(tokio::test)]
3163    async fn oversized_ttl_is_clamped_and_does_not_overflow() {
3164        let (server, db, _tg) = build_server();
3165        let mut dbtx = db.begin_transaction().await;
3166        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3167
3168        let gateway_id = random_pub_key();
3169        let mut ann = announcement(gateway_id, "https://gw/v1");
3170        ann.ttl = Duration::from_secs(u64::MAX);
3171
3172        server
3173            .register_gateway(&mut dbtx.to_ref_nc(), ann)
3174            .await
3175            .expect("must not panic on an absurd TTL");
3176
3177        let stored = dbtx
3178            .to_ref_nc()
3179            .get_value(&LightningGatewayKey(gateway_id))
3180            .await
3181            .expect("registration exists");
3182        assert!(
3183            stored.valid_until <= fedimint_core::time::now() + MAX_GATEWAY_REGISTRATION_TTL,
3184            "TTL must be clamped"
3185        );
3186    }
3187
3188    /// A gateway refreshing identical settings must not be locked out by its
3189    /// own stale nonce, e.g. after an NTP correction stepped its clock
3190    /// backwards.
3191    #[test_log::test(tokio::test)]
3192    async fn unchanged_registration_may_be_refreshed_with_a_stale_nonce() {
3193        let (server, db, _tg) = build_server();
3194        let mut dbtx = db.begin_transaction().await;
3195        let mut dbtx = dbtx.to_ref_with_prefix_module_id(42).0;
3196
3197        let gateway = Keypair::new(SECP256K1, &mut OsRng);
3198        let gateway_id = gateway.public_key();
3199        let ann = announcement(gateway_id, "https://gw/v1");
3200
3201        server
3202            .register_gateway(
3203                &mut dbtx.to_ref_nc(),
3204                sign(&server, &gateway, 500, ann.clone()),
3205            )
3206            .await
3207            .expect("initial registration accepted");
3208
3209        // Same settings, lower nonce: a refresh, not a rollback.
3210        server
3211            .register_gateway(&mut dbtx.to_ref_nc(), sign(&server, &gateway, 400, ann))
3212            .await
3213            .expect("identical settings may be refreshed regardless of nonce");
3214
3215        // But a *change* still requires the nonce to move forward.
3216        let err = server
3217            .register_gateway(
3218                &mut dbtx.to_ref_nc(),
3219                sign(
3220                    &server,
3221                    &gateway,
3222                    400,
3223                    announcement(gateway_id, "https://other.gw/v1"),
3224                ),
3225            )
3226            .await
3227            .expect_err("changing settings with a stale nonce must be rejected");
3228        assert!(err.to_string().contains("nonce must increase"));
3229
3230        assert_eq!(
3231            stored_api(&server, &mut dbtx.to_ref_nc(), gateway_id).await,
3232            "https://gw/v1"
3233        );
3234    }
3235}