From 4c84a4166dd4534b0360241c0525d07584435977 Mon Sep 17 00:00:00 2001 From: Duncan Dean Date: Tue, 12 Sep 2023 21:27:14 +0200 Subject: [PATCH] Add `OutboundV2Channel` struct --- lightning/src/ln/channel.rs | 193 +++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 35 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 8f629b2da3f..eb75a67f8f8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1397,7 +1397,10 @@ impl ChannelContext where SP::Target: SignerProvider { current_chain_height: u32, outbound_scid_alias: u64, temporary_channel_id: ChannelId, - channel_type: ChannelTypeFeatures, + holder_selected_channel_reserve_satoshis: u64, + channel_keys_id: [u8; 32], + holder_signer: ::EcdsaSigner, + pubkeys: ChannelPublicKeys, ) -> Result, APIError> where ES::Target: EntropySource, @@ -1408,9 +1411,6 @@ impl ChannelContext where SP::Target: SignerProvider { let channel_value_satoshis = funding_satoshis; let holder_selected_contest_delay = config.channel_handshake_config.our_to_self_delay; - let channel_keys_id = signer_provider.generate_channel_keys_id(false, channel_value_satoshis, user_id); - let holder_signer = signer_provider.derive_channel_signer(channel_value_satoshis, channel_keys_id); - let pubkeys = holder_signer.pubkeys().clone(); if !their_features.supports_wumbo() && channel_value_satoshis > MAX_FUNDING_SATOSHIS_NO_WUMBO { return Err(APIError::APIMisuseError{err: format!("funding_value must not exceed {}, it was {}", MAX_FUNDING_SATOSHIS_NO_WUMBO, channel_value_satoshis)}); @@ -1425,13 +1425,8 @@ impl ChannelContext where SP::Target: SignerProvider { if holder_selected_contest_delay < BREAKDOWN_TIMEOUT { return Err(APIError::APIMisuseError {err: format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks", holder_selected_contest_delay)}); } - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config); - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - // Protocol level safety check in place, although it should never happen because - // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` - return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); - } + let channel_type = get_initial_channel_type(&config, their_features); debug_assert!(channel_type.is_subset(&channelmanager::provided_channel_type_features(&config))); let (commitment_conf_target, anchor_outputs_value_msat) = if channel_type.supports_anchors_zero_fee_htlc_tx() { @@ -1487,6 +1482,7 @@ impl ChannelContext where SP::Target: SignerProvider { channel_state: ChannelState::OurInitSent as u32, announcement_sigs_state: AnnouncementSigsState::NotSent, secp_ctx, + // We'll add our counterparty's `funding_satoshis` when we receive `accept_channel2`. channel_value_satoshis, latest_monitor_update_id: 0, @@ -1520,6 +1516,8 @@ impl ChannelContext where SP::Target: SignerProvider { signer_pending_commitment_update: false, signer_pending_funding: false, + // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions + // when we receive `accept_channel2`. #[cfg(debug_assertions)] holder_max_commitment_tx_output: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), #[cfg(debug_assertions)] @@ -1540,6 +1538,8 @@ impl ChannelContext where SP::Target: SignerProvider { counterparty_dust_limit_satoshis: 0, holder_dust_limit_satoshis: MIN_CHAN_DUST_LIMIT_SATOSHIS, counterparty_max_htlc_value_in_flight_msat: 0, + // We'll adjust this to include our counterparty's `funding_satoshis` when we + // receive `accept_channel2`. holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis, &config.channel_handshake_config), counterparty_selected_channel_reserve_satoshis: None, // Filled in in accept_channel holder_selected_channel_reserve_satoshis, @@ -6604,7 +6604,17 @@ impl OutboundV1Channel where SP::Target: SignerProvider { F::Target: FeeEstimator { let temporary_channel_id = temporary_channel_id.unwrap_or_else(|| ChannelId::temporary_from_entropy_source(entropy_source)); - let channel_type = Self::get_initial_channel_type(&config, their_features); + + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config); + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { + // Protocol level safety check in place, although it should never happen because + // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` + return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); + } + + let channel_keys_id = signer_provider.generate_channel_keys_id(false, channel_value_satoshis, user_id); + let holder_signer = signer_provider.derive_channel_signer(channel_value_satoshis, channel_keys_id); + let pubkeys = holder_signer.pubkeys().clone(); let chan = Self { context: ChannelContext::new_for_outbound_channel( @@ -6620,7 +6630,10 @@ impl OutboundV1Channel where SP::Target: SignerProvider { current_chain_height, outbound_scid_alias, temporary_channel_id, - channel_type, + holder_selected_channel_reserve_satoshis, + channel_keys_id, + holder_signer, + pubkeys, )?, unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 } }; @@ -6684,29 +6697,6 @@ impl OutboundV1Channel where SP::Target: SignerProvider { Ok((channel, funding_created)) } - fn get_initial_channel_type(config: &UserConfig, their_features: &InitFeatures) -> ChannelTypeFeatures { - // The default channel type (ie the first one we try) depends on whether the channel is - // public - if it is, we just go with `only_static_remotekey` as it's the only option - // available. If it's private, we first try `scid_privacy` as it provides better privacy - // with no other changes, and fall back to `only_static_remotekey`. - let mut ret = ChannelTypeFeatures::only_static_remote_key(); - if !config.channel_handshake_config.announced_channel && - config.channel_handshake_config.negotiate_scid_privacy && - their_features.supports_scid_privacy() { - ret.set_scid_privacy_required(); - } - - // Optionally, if the user would like to negotiate the `anchors_zero_fee_htlc_tx` option, we - // set it now. If they don't understand it, we'll fall back to our default of - // `only_static_remotekey`. - if config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx && - their_features.supports_anchors_zero_fee_htlc_tx() { - ret.set_anchors_zero_fee_htlc_tx_required(); - } - - ret - } - /// If we receive an error message, it may only be a rejection of the channel type we tried, /// not of our ability to open any channel at all. Thus, on error, we should first call this /// and see if we get a new `OpenChannel` message, otherwise the channel is failed. @@ -7161,6 +7151,114 @@ impl InboundV1Channel where SP::Target: SignerProvider { } } +// A not-yet-funded outbound (from holder) channel using V2 channel establishment. +pub(super) struct OutboundV2Channel where SP::Target: SignerProvider { + pub context: ChannelContext, + pub unfunded_context: UnfundedChannelContext, + #[cfg(dual_funding)] + pub dual_funding_context: DualFundingChannelContext, +} + +#[cfg(dual_funding)] +impl OutboundV2Channel where SP::Target: SignerProvider { + pub fn new( + fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, + counterparty_node_id: PublicKey, their_features: &InitFeatures, funding_satoshis: u64, + user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, + funding_confirmation_target: ConfirmationTarget, + ) -> Result, APIError> + where ES::Target: EntropySource, + F::Target: FeeEstimator, + { + let channel_keys_id = signer_provider.generate_channel_keys_id(false, funding_satoshis, user_id); + let holder_signer = signer_provider.derive_channel_signer(funding_satoshis, channel_keys_id); + let pubkeys = holder_signer.pubkeys().clone(); + + let temporary_channel_id = ChannelId::temporary_v2_from_revocation_basepoint(&pubkeys.revocation_basepoint); + + let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + + let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); + let funding_tx_locktime = current_chain_height; + + let chan = Self { + context: ChannelContext::new_for_outbound_channel( + fee_estimator, + entropy_source, + signer_provider, + counterparty_node_id, + their_features, + funding_satoshis, + 0, + user_id, + config, + current_chain_height, + outbound_scid_alias, + temporary_channel_id, + holder_selected_channel_reserve_satoshis, + channel_keys_id, + holder_signer, + pubkeys, + )?, + unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }, + dual_funding_context: DualFundingChannelContext { + our_funding_satoshis: funding_satoshis, + their_funding_satoshis: 0, + funding_tx_locktime, + funding_feerate_sat_per_1000_weight, + } + }; + Ok(chan) + } + + pub fn get_open_channel_v2(&self, chain_hash: ChainHash) -> msgs::OpenChannelV2 { + if self.context.channel_state != ChannelState::OurInitSent as u32 { + panic!("Cannot generate an open_channel2 after we've moved forward"); + } + + if self.context.cur_holder_commitment_transaction_number != INITIAL_COMMITMENT_NUMBER { + panic!("Tried to send an open_channel2 for a channel that has already advanced"); + } + + let first_per_commitment_point = self.context.holder_signer.as_ref() + .get_per_commitment_point(self.context.cur_holder_commitment_transaction_number, + &self.context.secp_ctx); + let second_per_commitment_point = self.context.holder_signer.as_ref() + .get_per_commitment_point(self.context.cur_holder_commitment_transaction_number - 1, + &self.context.secp_ctx); + let keys = self.context.get_holder_pubkeys(); + + msgs::OpenChannelV2 { + chain_hash, + temporary_channel_id: self.context.temporary_channel_id.unwrap(), + funding_satoshis: self.context.channel_value_satoshis, + dust_limit_satoshis: self.context.holder_dust_limit_satoshis, + max_htlc_value_in_flight_msat: self.context.holder_max_htlc_value_in_flight_msat, + htlc_minimum_msat: self.context.holder_htlc_minimum_msat, + funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, + commitment_feerate_sat_per_1000_weight: self.context.feerate_per_kw, + to_self_delay: self.context.get_holder_selected_contest_delay(), + max_accepted_htlcs: self.context.holder_max_accepted_htlcs, + funding_pubkey: keys.funding_pubkey, + revocation_basepoint: keys.revocation_basepoint.to_public_key(), + payment_basepoint: keys.payment_point, + delayed_payment_basepoint: keys.delayed_payment_basepoint.to_public_key(), + htlc_basepoint: keys.htlc_basepoint.to_public_key(), + first_per_commitment_point, + second_per_commitment_point, + channel_flags: if self.context.config.announced_channel {1} else {0}, + shutdown_scriptpubkey: Some(match &self.context.shutdown_scriptpubkey { + Some(script) => script.clone().into_inner(), + None => Builder::new().into_script(), + }), + channel_type: Some(self.context.channel_type.clone()), + locktime: self.dual_funding_context.funding_tx_locktime, + require_confirmed_inputs: None, + } + } +} + // A not-yet-funded inbound (from counterparty) channel using V2 channel establishment. #[cfg(dual_funding)] pub(super) struct InboundV2Channel where SP::Target: SignerProvider { @@ -7314,6 +7412,31 @@ impl InboundV2Channel where SP::Target: SignerProvider { } } +// Unfunded channel utilities + +fn get_initial_channel_type(config: &UserConfig, their_features: &InitFeatures) -> ChannelTypeFeatures { + // The default channel type (ie the first one we try) depends on whether the channel is + // public - if it is, we just go with `only_static_remotekey` as it's the only option + // available. If it's private, we first try `scid_privacy` as it provides better privacy + // with no other changes, and fall back to `only_static_remotekey`. + let mut ret = ChannelTypeFeatures::only_static_remote_key(); + if !config.channel_handshake_config.announced_channel && + config.channel_handshake_config.negotiate_scid_privacy && + their_features.supports_scid_privacy() { + ret.set_scid_privacy_required(); + } + + // Optionally, if the user would like to negotiate the `anchors_zero_fee_htlc_tx` option, we + // set it now. If they don't understand it, we'll fall back to our default of + // `only_static_remotekey`. + if config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx && + their_features.supports_anchors_zero_fee_htlc_tx() { + ret.set_anchors_zero_fee_htlc_tx_required(); + } + + ret +} + const SERIALIZATION_VERSION: u8 = 3; const MIN_SERIALIZATION_VERSION: u8 = 3;