From da68f2c1ae93d493253ef72ad19f682e0669d044 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 5 Mar 2026 14:41:59 -0600 Subject: [PATCH 1/4] Add PendingV1 channel phase between UnfundedOutboundV1 and Funded Introduce PendingV1Channel to represent the intermediate state where funding_created has been generated but funding_signed has not yet been received. This makes the channel state machine more explicit: UnfundedOutboundV1 -> PendingV1 -> Funded Move get_funding_created_msg, funding_signed, signer_maybe_unblocked (funding path), unset_funding_info, and the InitialRemoteCommitmentReceiver impl from OutboundV1Channel to PendingV1Channel. Change OutboundV1Channel::get_funding_created to consume self and return (PendingV1Channel, Option). Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 236 +++++++++++++++++-------- lightning/src/ln/channel_open_tests.rs | 18 +- lightning/src/ln/channelmanager.rs | 2 +- 3 files changed, 171 insertions(+), 85 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 9361cd3c749..731233324be 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1488,6 +1488,7 @@ pub(super) struct Channel { enum ChannelPhase { Undefined, UnfundedOutboundV1(OutboundV1Channel), + PendingV1(PendingV1Channel), UnfundedInboundV1(InboundV1Channel), UnfundedV2(PendingV2Channel), Funded(FundedChannel), @@ -1502,6 +1503,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => &chan.context, ChannelPhase::UnfundedOutboundV1(chan) => &chan.context, + ChannelPhase::PendingV1(chan) => &chan.context, ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedV2(chan) => &chan.context, } @@ -1512,6 +1514,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => &mut chan.context, ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.context, + ChannelPhase::PendingV1(chan) => &mut chan.context, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedV2(chan) => &mut chan.context, } @@ -1522,6 +1525,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => &chan.funding, ChannelPhase::UnfundedOutboundV1(chan) => &chan.funding, + ChannelPhase::PendingV1(chan) => &chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, ChannelPhase::UnfundedV2(chan) => &chan.funding, } @@ -1533,6 +1537,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => &mut chan.funding, ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.funding, + ChannelPhase::PendingV1(chan) => &mut chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedV2(chan) => &mut chan.funding, } @@ -1543,6 +1548,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedOutboundV1(chan) => (&chan.funding, &mut chan.context), + ChannelPhase::PendingV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), } @@ -1556,6 +1562,7 @@ where None }, ChannelPhase::UnfundedOutboundV1(chan) => Some(&mut chan.unfunded_context), + ChannelPhase::PendingV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedInboundV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedV2(chan) => Some(&mut chan.unfunded_context), } @@ -1593,7 +1600,9 @@ where pub fn is_unfunded_v1(&self) -> bool { matches!( self.phase, - ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) + ChannelPhase::UnfundedOutboundV1(_) + | ChannelPhase::PendingV1(_) + | ChannelPhase::UnfundedInboundV1(_) ) } @@ -1645,12 +1654,30 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => chan.signer_maybe_unblocked(logger, path_for_release_htlc).map(|r| Some(r)), ChannelPhase::UnfundedOutboundV1(chan) => { - let (open_channel, funding_created) = chan.signer_maybe_unblocked(chain_hash, logger); + let open_channel = chan.signer_maybe_unblocked(chain_hash, logger); Ok(Some(SignerResumeUpdates { commitment_update: None, revoke_and_ack: None, open_channel, accept_channel: None, + funding_created: None, + funding_signed: None, + funding_commit_sig: None, + tx_signatures: None, + channel_ready: None, + order: chan.context.resend_order.clone(), + closing_signed: None, + signed_closing_tx: None, + shutdown_result: None, + })) + }, + ChannelPhase::PendingV1(chan) => { + let funding_created = chan.signer_maybe_unblocked(logger); + Ok(Some(SignerResumeUpdates { + commitment_update: None, + revoke_and_ack: None, + open_channel: None, + accept_channel: None, funding_created, funding_signed: None, funding_commit_sig: None, @@ -1700,6 +1727,7 @@ where // handshake (and bailing if the peer rejects it), so we force-close in // that case. ChannelPhase::UnfundedOutboundV1(chan) => chan.is_resumable(), + ChannelPhase::PendingV1(_) => false, ChannelPhase::UnfundedInboundV1(_) => false, ChannelPhase::UnfundedV2(_) => false, }; @@ -1749,6 +1777,11 @@ where .map(|msg| ReconnectionMsg::Open(OpenChannelMessage::V1(msg))) .unwrap_or(ReconnectionMsg::None) }, + ChannelPhase::PendingV1(_) => { + // PendingV1 channels are not resumable, so this shouldn't be reached. + debug_assert!(false); + ReconnectionMsg::None + }, ChannelPhase::UnfundedInboundV1(_) => { // Since unfunded inbound channel maps are cleared upon disconnecting a peer, // they are not persisted and won't be recovered after a crash. @@ -1787,6 +1820,7 @@ where ) .map(|msg| Some(OpenChannelMessage::V1(msg))) }, + ChannelPhase::PendingV1(_) => Ok(None), ChannelPhase::UnfundedInboundV1(_) => Ok(None), ChannelPhase::UnfundedV2(chan) => { if chan.funding.is_outbound() { @@ -1817,9 +1851,9 @@ where let (splice_funding_failed, exited_quiescence) = match &mut self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => { - (None, false) - }, + ChannelPhase::UnfundedOutboundV1(_) + | ChannelPhase::PendingV1(_) + | ChannelPhase::UnfundedInboundV1(_) => (None, false), ChannelPhase::UnfundedV2(pending_v2_channel) => { pending_v2_channel.interactive_tx_constructor.take(); (None, false) @@ -1990,7 +2024,9 @@ where // https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L578-L580 let (should_ack, splice_funding_failed, exited_quiescence) = match &mut self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => { + ChannelPhase::UnfundedOutboundV1(_) + | ChannelPhase::PendingV1(_) + | ChannelPhase::UnfundedInboundV1(_) => { let err = "Got an unexpected tx_abort message: This is an unfunded channel created with V1 channel establishment"; return Err(ChannelError::Warn(err.into())); }, @@ -2053,7 +2089,7 @@ where &mut self, msg: &msgs::FundingSigned, best_block: BestBlock, signer_provider: &SP, logger: &L ) -> Result<(&mut FundedChannel, ChannelMonitor), ChannelError> { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); - let result = if let ChannelPhase::UnfundedOutboundV1(chan) = phase { + let result = if let ChannelPhase::PendingV1(chan) = phase { let channel_state = chan.context.channel_state; let logger = WithChannelContext::from(logger, &chan.context, None); match chan.funding_signed(msg, best_block, signer_provider, &&logger) { @@ -2064,13 +2100,13 @@ where }, Err((chan, e)) => { debug_assert_eq!(chan.context.channel_state, channel_state); - self.phase = ChannelPhase::UnfundedOutboundV1(chan); + self.phase = ChannelPhase::PendingV1(chan); Err(e) }, } } else { self.phase = phase; - Err(ChannelError::SendError("Failed to find corresponding UnfundedOutboundV1 channel".to_owned())) + Err(ChannelError::SendError("Failed to find corresponding PendingV1 channel".to_owned())) }; debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); @@ -2438,6 +2474,9 @@ where ChannelPhase::UnfundedOutboundV1(chan) => { chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator) }, + ChannelPhase::PendingV1(chan) => { + chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator) + }, ChannelPhase::UnfundedInboundV1(chan) => { chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator) }, @@ -2461,6 +2500,15 @@ where } } +impl From> for Channel +where + SP::EcdsaSigner: ChannelSigner, +{ + fn from(channel: PendingV1Channel) -> Self { + Channel { phase: ChannelPhase::PendingV1(channel) } + } +} + impl From> for Channel where SP::EcdsaSigner: ChannelSigner, @@ -3476,7 +3524,7 @@ trait InitialRemoteCommitmentReceiver { fn is_v2_established(&self) -> bool; } -impl InitialRemoteCommitmentReceiver for OutboundV1Channel { +impl InitialRemoteCommitmentReceiver for PendingV1Channel { fn context(&self) -> &ChannelContext { &self.context } @@ -13311,45 +13359,6 @@ impl OutboundV1Channel { Ok(chan) } - /// Only allowed after [`FundingScope::channel_transaction_parameters`] is set. - #[rustfmt::skip] - fn get_funding_created_msg(&mut self, logger: &L) -> Option { - let commitment_data = self.context.build_commitment_transaction(&self.funding, - self.context.counterparty_next_commitment_transaction_number, - &self.context.counterparty_next_commitment_point.unwrap(), false, false, logger); - let counterparty_initial_commitment_tx = commitment_data.tx; - let signature = match &self.context.holder_signer { - // TODO (taproot|arik): move match into calling method for Taproot - ChannelSignerType::Ecdsa(ecdsa) => { - let channel_parameters = &self.funding.channel_transaction_parameters; - ecdsa.sign_counterparty_commitment(channel_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.context.secp_ctx) - .map(|(sig, _)| sig).ok() - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() - }; - - if signature.is_some() && self.context.signer_pending_funding { - log_trace!(logger, "Counterparty commitment signature ready for funding_created message: clearing signer_pending_funding"); - self.context.signer_pending_funding = false; - } else if signature.is_none() { - log_trace!(logger, "funding_created awaiting signer; setting signer_pending_funding"); - self.context.signer_pending_funding = true; - }; - - signature.map(|signature| msgs::FundingCreated { - temporary_channel_id: self.context.temporary_channel_id.unwrap(), - funding_txid: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().txid, - funding_output_index: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().index, - signature, - #[cfg(taproot)] - partial_signature_with_nonce: None, - #[cfg(taproot)] - next_local_nonce: None, - }) - } - /// Updates channel state with knowledge of the funding transaction's txid/index, and generates /// a funding_created message for the remote peer. /// Panics if called at some time other than immediately after initial handshake, if called twice, @@ -13358,8 +13367,8 @@ impl OutboundV1Channel { /// Do NOT broadcast the funding transaction until after a successful funding_signed call! /// If an Err is returned, it is a ChannelError::Close. #[rustfmt::skip] - pub fn get_funding_created(&mut self, funding_transaction: Transaction, funding_txo: OutPoint, is_batch_funding: bool, logger: &L) - -> Result, (Self, ChannelError)> { + pub fn get_funding_created(mut self, funding_transaction: Transaction, funding_txo: OutPoint, is_batch_funding: bool, logger: &L) + -> Result<(PendingV1Channel, Option), (Self, ChannelError)> { if !self.funding.is_outbound() { panic!("Tried to create outbound funding_created message on an inbound channel!"); } @@ -13390,8 +13399,13 @@ impl OutboundV1Channel { self.funding.funding_transaction = Some(funding_transaction); self.context.is_batch_funding = Some(()).filter(|_| is_batch_funding); - let funding_created = self.get_funding_created_msg(logger); - Ok(funding_created) + let mut pending = PendingV1Channel { + funding: self.funding, + context: self.context, + unfunded_context: self.unfunded_context, + }; + let funding_created = pending.get_funding_created_msg(logger); + Ok((pending, funding_created)) } /// If we receive an error message, it may only be a rejection of the channel type we tried, @@ -13485,6 +13499,81 @@ impl OutboundV1Channel { ) } + /// Indicates that the signer may have some signatures for us, so we should retry if we're + /// blocked. + #[rustfmt::skip] + pub fn signer_maybe_unblocked( + &mut self, chain_hash: ChainHash, logger: &L + ) -> Option { + // If we were pending a commitment point, retry the signer and advance to an + // available state. + if self.unfunded_context.holder_commitment_point.is_none() { + self.unfunded_context.holder_commitment_point = HolderCommitmentPoint::new(&self.context.holder_signer, &self.context.secp_ctx); + } + if let Some(ref mut point) = self.unfunded_context.holder_commitment_point { + if !point.can_advance() { + point.try_resolve_pending(&self.context.holder_signer, &self.context.secp_ctx, logger); + } + } + if self.signer_pending_open_channel { + log_trace!(logger, "Attempting to generate open_channel..."); + self.get_open_channel(chain_hash, logger) + } else { None } + } +} + +/// An outbound channel using V1 channel establishment that has generated a `funding_created` +/// message but has not yet received `funding_signed`. +pub(super) struct PendingV1Channel { + pub funding: FundingScope, + pub context: ChannelContext, + pub unfunded_context: UnfundedChannelContext, +} + +impl PendingV1Channel { + pub fn abandon_unfunded_chan(&mut self, closure_reason: ClosureReason) -> ShutdownResult { + self.context.force_shutdown(&self.funding, closure_reason) + } + + /// Only allowed after [`FundingScope::channel_transaction_parameters`] is set. + #[rustfmt::skip] + fn get_funding_created_msg(&mut self, logger: &L) -> Option { + let commitment_data = self.context.build_commitment_transaction(&self.funding, + self.context.counterparty_next_commitment_transaction_number, + &self.context.counterparty_next_commitment_point.unwrap(), false, false, logger); + let counterparty_initial_commitment_tx = commitment_data.tx; + let signature = match &self.context.holder_signer { + // TODO (taproot|arik): move match into calling method for Taproot + ChannelSignerType::Ecdsa(ecdsa) => { + let channel_parameters = &self.funding.channel_transaction_parameters; + ecdsa.sign_counterparty_commitment(channel_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.context.secp_ctx) + .map(|(sig, _)| sig).ok() + }, + // TODO (taproot|arik) + #[cfg(taproot)] + _ => todo!() + }; + + if signature.is_some() && self.context.signer_pending_funding { + log_trace!(logger, "Counterparty commitment signature ready for funding_created message: clearing signer_pending_funding"); + self.context.signer_pending_funding = false; + } else if signature.is_none() { + log_trace!(logger, "funding_created awaiting signer; setting signer_pending_funding"); + self.context.signer_pending_funding = true; + }; + + signature.map(|signature| msgs::FundingCreated { + temporary_channel_id: self.context.temporary_channel_id.unwrap(), + funding_txid: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().txid, + funding_output_index: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().index, + signature, + #[cfg(taproot)] + partial_signature_with_nonce: None, + #[cfg(taproot)] + next_local_nonce: None, + }) + } + /// Handles a funding_signed message from the remote end. /// If this call is successful, broadcast the funding transaction (and not before!) pub fn funding_signed( @@ -13492,7 +13581,7 @@ impl OutboundV1Channel { logger: &L, ) -> Result< (FundedChannel, ChannelMonitor), - (OutboundV1Channel, ChannelError), + (PendingV1Channel, ChannelError), > { if !self.funding.is_outbound() { let err = "Received funding_signed for an inbound channel?"; @@ -13556,29 +13645,24 @@ impl OutboundV1Channel { /// Indicates that the signer may have some signatures for us, so we should retry if we're /// blocked. - #[rustfmt::skip] pub fn signer_maybe_unblocked( - &mut self, chain_hash: ChainHash, logger: &L - ) -> (Option, Option) { - // If we were pending a commitment point, retry the signer and advance to an - // available state. - if self.unfunded_context.holder_commitment_point.is_none() { - self.unfunded_context.holder_commitment_point = HolderCommitmentPoint::new(&self.context.holder_signer, &self.context.secp_ctx); - } + &mut self, logger: &L, + ) -> Option { if let Some(ref mut point) = self.unfunded_context.holder_commitment_point { if !point.can_advance() { - point.try_resolve_pending(&self.context.holder_signer, &self.context.secp_ctx, logger); + point.try_resolve_pending( + &self.context.holder_signer, + &self.context.secp_ctx, + logger, + ); } } - let open_channel = if self.signer_pending_open_channel { - log_trace!(logger, "Attempting to generate open_channel..."); - self.get_open_channel(chain_hash, logger) - } else { None }; - let funding_created = if self.context.signer_pending_funding && self.funding.is_outbound() { + if self.context.signer_pending_funding { log_trace!(logger, "Attempting to generate pending funding created..."); self.get_funding_created_msg(logger) - } else { None }; - (open_channel, funding_created) + } else { + None + } } /// Unsets the existing funding information. @@ -15926,7 +16010,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let funding_created_msg = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (_, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16069,7 +16153,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let funding_created_msg = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (mut node_b_chan, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16265,7 +16349,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let funding_created_msg = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (_, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16374,11 +16458,11 @@ mod tests { }], }; let funding_outpoint = OutPoint { txid: tx.compute_txid(), index: 0 }; - let funding_created = outbound_chan + let (_pending, funding_created) = outbound_chan .get_funding_created(tx.clone(), funding_outpoint, false, &&logger) .map_err(|_| ()) - .unwrap() .unwrap(); + let funding_created = funding_created.unwrap(); let mut chan = match inbound_chan.funding_created( &funding_created, best_block, @@ -18034,7 +18118,7 @@ mod tests { }, ]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let funding_created_msg = node_a_chan.get_funding_created( + let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created( tx.clone(), funding_outpoint, true, &&logger, ).map_err(|_| ()).unwrap(); let (mut node_b_chan, funding_signed_msg, _) = node_b_chan.funding_created( diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 08cabc053c5..c5a5f6749ab 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -1509,15 +1509,17 @@ pub fn test_duplicate_chan_id() { let chan_id = open_chan_2_msg.common_fields.temporary_channel_id; let mut channel = a_peer_state.channel_by_id.remove(&chan_id).unwrap(); - if let Some(mut chan) = channel.as_unfunded_outbound_v1_mut() { - let logger = test_utils::TestLogger::new(); - chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger) - .map_err(|_| ()) - .unwrap() - } else { - panic!("Unexpected Channel phase") + match channel.into_unfunded_outbound_v1() { + Ok(chan) => { + let logger = test_utils::TestLogger::new(); + let (_pending, funding_created) = chan + .get_funding_created(tx.clone(), funding_outpoint, false, &&logger) + .map_err(|_| ()) + .unwrap(); + funding_created.unwrap() + }, + Err(_) => panic!("Unexpected Channel phase"), } - .unwrap() }; check_added_monitors(&nodes[0], 0); nodes[1].node.handle_funding_created(node_a_id, &funding_created); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ada27af749f..93c4cddc561 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6120,7 +6120,7 @@ impl< let logger = WithChannelContext::from(&self.logger, &chan.context, None); let funding_res = chan.get_funding_created(funding_transaction, funding_txo, is_batch_funding, &&logger); let (mut chan, msg_opt) = match funding_res { - Ok(funding_msg) => (chan, funding_msg), + Ok((pending, funding_msg)) => (pending, funding_msg), Err((mut chan, chan_err)) => { let api_err = APIError::ChannelUnavailable { err: "Signer refused to sign the initial commitment transaction".to_owned() }; return abandon_chan!(chan_err, api_err, chan); From 717c8544c7a41668a21469feeeeab4d215a90e78 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sun, 1 Mar 2026 23:08:41 -0600 Subject: [PATCH 2/4] Rename PendingV2Channel to UnfundedV2Channel Mechanical rename to free the PendingV2Channel name for a new struct that will hold V2 channels with complete funding parameters, following the PendingV1 pattern. Co-Authored-By: Claude Opus 4.6 --- lightning/src/ln/channel.rs | 14 +++++++------- lightning/src/ln/channelmanager.rs | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 731233324be..18f8a0c55ca 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1490,7 +1490,7 @@ enum ChannelPhase { UnfundedOutboundV1(OutboundV1Channel), PendingV1(PendingV1Channel), UnfundedInboundV1(InboundV1Channel), - UnfundedV2(PendingV2Channel), + UnfundedV2(UnfundedV2Channel), Funded(FundedChannel), } @@ -1638,7 +1638,7 @@ where } } - pub fn as_unfunded_v2(&self) -> Option<&PendingV2Channel> { + pub fn as_unfunded_v2(&self) -> Option<&UnfundedV2Channel> { if let ChannelPhase::UnfundedV2(channel) = &self.phase { Some(channel) } else { @@ -2135,7 +2135,7 @@ where chan.interactive_tx_constructor .take() - .expect("PendingV2Channel::interactive_tx_constructor should be set") + .expect("UnfundedV2Channel::interactive_tx_constructor should be set") }, ChannelPhase::Funded(chan) => { if let Some(pending_splice) = chan.pending_splice.as_mut() { @@ -2518,11 +2518,11 @@ where } } -impl From> for Channel +impl From> for Channel where SP::EcdsaSigner: ChannelSigner, { - fn from(channel: PendingV2Channel) -> Self { + fn from(channel: UnfundedV2Channel) -> Self { Channel { phase: ChannelPhase::UnfundedV2(channel) } } } @@ -13960,7 +13960,7 @@ impl InboundV1Channel { } // A not-yet-funded channel using V2 channel establishment. -pub(super) struct PendingV2Channel { +pub(super) struct UnfundedV2Channel { pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, @@ -13969,7 +13969,7 @@ pub(super) struct PendingV2Channel { pub interactive_tx_constructor: Option, } -impl PendingV2Channel { +impl UnfundedV2Channel { #[allow(dead_code)] // TODO(dual_funding): Remove once creating V2 channels is enabled. #[rustfmt::skip] pub fn new_outbound( diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 93c4cddc561..c6764917b8d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -61,8 +61,8 @@ use crate::ln::channel::QuiescentError; use crate::ln::channel::{ self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, DisconnectResult, FundedChannel, FundingTxSigned, InboundV1Channel, InteractiveTxMsgError, OutboundHop, - OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, SpliceFundingFailed, - StfuResponse, UpdateFulfillCommitFetch, WithChannelContext, + OutboundV1Channel, ReconnectionMsg, ShutdownResult, SpliceFundingFailed, StfuResponse, + UnfundedV2Channel, UpdateFulfillCommitFetch, WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; use crate::ln::funding::{FundingContribution, FundingTemplate}; @@ -10834,7 +10834,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); (*temporary_channel_id, Channel::from(channel), message_send_event) }), - OpenChannelMessage::V2(open_channel_msg) => PendingV2Channel::new_inbound( + OpenChannelMessage::V2(open_channel_msg) => UnfundedV2Channel::new_inbound( &self.fee_estimator, &self.entropy_source, &self.signer_provider, From c6c28f755a60b336a24470b8bef61a6cd62abf77 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 5 Mar 2026 14:51:29 -0600 Subject: [PATCH 3/4] Add PendingV2 channel phase between UnfundedV2 and Funded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a PendingV2Channel struct and ChannelPhase::PendingV2 variant to represent V2 channels that have completed funding transaction construction but have not yet received commitment_signed. The funding_tx_constructed method now performs the UnfundedV2 → PendingV2 phase transition using core::mem::replace, and funding_transaction_signed and commitment_signed now match on PendingV2 instead of UnfundedV2. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 116 ++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 32 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 18f8a0c55ca..4a8dc93fbd8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1491,6 +1491,7 @@ enum ChannelPhase { PendingV1(PendingV1Channel), UnfundedInboundV1(InboundV1Channel), UnfundedV2(UnfundedV2Channel), + PendingV2(PendingV2Channel), Funded(FundedChannel), } @@ -1506,6 +1507,7 @@ where ChannelPhase::PendingV1(chan) => &chan.context, ChannelPhase::UnfundedInboundV1(chan) => &chan.context, ChannelPhase::UnfundedV2(chan) => &chan.context, + ChannelPhase::PendingV2(chan) => &chan.context, } } @@ -1517,6 +1519,7 @@ where ChannelPhase::PendingV1(chan) => &mut chan.context, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.context, ChannelPhase::UnfundedV2(chan) => &mut chan.context, + ChannelPhase::PendingV2(chan) => &mut chan.context, } } @@ -1528,6 +1531,7 @@ where ChannelPhase::PendingV1(chan) => &chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, ChannelPhase::UnfundedV2(chan) => &chan.funding, + ChannelPhase::PendingV2(chan) => &chan.funding, } } @@ -1540,6 +1544,7 @@ where ChannelPhase::PendingV1(chan) => &mut chan.funding, ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, ChannelPhase::UnfundedV2(chan) => &mut chan.funding, + ChannelPhase::PendingV2(chan) => &mut chan.funding, } } @@ -1551,6 +1556,7 @@ where ChannelPhase::PendingV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), + ChannelPhase::PendingV2(chan) => (&chan.funding, &mut chan.context), } } @@ -1565,6 +1571,7 @@ where ChannelPhase::PendingV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedInboundV1(chan) => Some(&mut chan.unfunded_context), ChannelPhase::UnfundedV2(chan) => Some(&mut chan.unfunded_context), + ChannelPhase::PendingV2(chan) => Some(&mut chan.unfunded_context), } } @@ -1707,7 +1714,7 @@ where shutdown_result: None, })) }, - ChannelPhase::UnfundedV2(_) => Ok(None), + ChannelPhase::UnfundedV2(_) | ChannelPhase::PendingV2(_) => Ok(None), } } @@ -1730,6 +1737,7 @@ where ChannelPhase::PendingV1(_) => false, ChannelPhase::UnfundedInboundV1(_) => false, ChannelPhase::UnfundedV2(_) => false, + ChannelPhase::PendingV2(_) => false, }; let splice_funding_failed = if let ChannelPhase::Funded(chan) = &mut self.phase { @@ -1777,15 +1785,8 @@ where .map(|msg| ReconnectionMsg::Open(OpenChannelMessage::V1(msg))) .unwrap_or(ReconnectionMsg::None) }, - ChannelPhase::PendingV1(_) => { - // PendingV1 channels are not resumable, so this shouldn't be reached. - debug_assert!(false); - ReconnectionMsg::None - }, - ChannelPhase::UnfundedInboundV1(_) => { - // Since unfunded inbound channel maps are cleared upon disconnecting a peer, - // they are not persisted and won't be recovered after a crash. - // Therefore, they shouldn't exist at this point. + ChannelPhase::PendingV1(_) | ChannelPhase::PendingV2(_) => { + // Pending channels are not resumable, so this shouldn't be reached. debug_assert!(false); ReconnectionMsg::None }, @@ -1802,6 +1803,13 @@ where ReconnectionMsg::None } }, + ChannelPhase::UnfundedInboundV1(_) => { + // Since unfunded inbound channel maps are cleared upon disconnecting a peer, + // they are not persisted and won't be recovered after a crash. + // Therefore, they shouldn't exist at this point. + debug_assert!(false); + ReconnectionMsg::None + }, } } @@ -1820,7 +1828,7 @@ where ) .map(|msg| Some(OpenChannelMessage::V1(msg))) }, - ChannelPhase::PendingV1(_) => Ok(None), + ChannelPhase::PendingV1(_) | ChannelPhase::PendingV2(_) => Ok(None), ChannelPhase::UnfundedInboundV1(_) => Ok(None), ChannelPhase::UnfundedV2(chan) => { if chan.funding.is_outbound() { @@ -1853,6 +1861,7 @@ where ChannelPhase::Undefined => unreachable!(), ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::PendingV1(_) + | ChannelPhase::PendingV2(_) | ChannelPhase::UnfundedInboundV1(_) => (None, false), ChannelPhase::UnfundedV2(pending_v2_channel) => { pending_v2_channel.interactive_tx_constructor.take(); @@ -2030,6 +2039,10 @@ where let err = "Got an unexpected tx_abort message: This is an unfunded channel created with V1 channel establishment"; return Err(ChannelError::Warn(err.into())); }, + ChannelPhase::PendingV2(chan) => { + let had_session = chan.context.interactive_tx_signing_session.take().is_some(); + (had_session, None, false) + }, ChannelPhase::UnfundedV2(pending_v2_channel) => { let had_constructor = pending_v2_channel.interactive_tx_constructor.take().is_some(); @@ -2114,8 +2127,9 @@ where } fn funding_tx_constructed(&mut self, funding_outpoint: OutPoint) -> Result<(), AbortReason> { - let interactive_tx_constructor = match &mut self.phase { - ChannelPhase::UnfundedV2(chan) => { + let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); + let result = match phase { + ChannelPhase::UnfundedV2(mut chan) => { debug_assert_eq!( chan.context.channel_state, ChannelState::NegotiatingFunding( @@ -2133,12 +2147,23 @@ where chan.funding.channel_transaction_parameters.funding_outpoint = Some(funding_outpoint); - chan.interactive_tx_constructor + let interactive_tx_constructor = chan + .interactive_tx_constructor .take() - .expect("UnfundedV2Channel::interactive_tx_constructor should be set") + .expect("UnfundedV2Channel::interactive_tx_constructor should be set"); + + let signing_session = interactive_tx_constructor.into_signing_session(); + chan.context.interactive_tx_signing_session = Some(signing_session); + + self.phase = ChannelPhase::PendingV2(PendingV2Channel { + funding: chan.funding, + context: chan.context, + unfunded_context: chan.unfunded_context, + }); + Ok(()) }, - ChannelPhase::Funded(chan) => { - if let Some(pending_splice) = chan.pending_splice.as_mut() { + ChannelPhase::Funded(mut chan) => { + let result = if let Some(pending_splice) = chan.pending_splice.as_mut() { let funding_negotiation = pending_splice.funding_negotiation.take(); if let Some(FundingNegotiation::ConstructingTransaction { mut funding, @@ -2154,31 +2179,32 @@ where funding, initial_commitment_signed_from_counterparty: None, }); - interactive_tx_constructor + + let signing_session = interactive_tx_constructor.into_signing_session(); + chan.context.interactive_tx_signing_session = Some(signing_session); + Ok(()) } else { // Replace the taken state for later error handling pending_splice.funding_negotiation = funding_negotiation; - return Err(AbortReason::InternalError( + Err(AbortReason::InternalError( "Got a tx_complete message in an invalid state", - )); + )) } } else { - return Err(AbortReason::InternalError( - "Got a tx_complete message in an invalid state", - )); - } + Err(AbortReason::InternalError("Got a tx_complete message in an invalid state")) + }; + self.phase = ChannelPhase::Funded(chan); + result }, _ => { + self.phase = phase; debug_assert!(false); - return Err(AbortReason::InternalError( - "Got a tx_complete message in an invalid phase", - )); + Err(AbortReason::InternalError("Got a tx_complete message in an invalid phase")) }, }; - let signing_session = interactive_tx_constructor.into_signing_session(); - self.context_mut().interactive_tx_signing_session = Some(signing_session); - Ok(()) + debug_assert!(!matches!(self.phase, ChannelPhase::Undefined)); + result } pub fn funding_transaction_signed( @@ -2187,7 +2213,7 @@ where ) -> Result { let (context, funding, pending_splice) = match &mut self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::UnfundedV2(channel) => (&mut channel.context, &channel.funding, None), + ChannelPhase::PendingV2(channel) => (&mut channel.context, &channel.funding, None), ChannelPhase::Funded(channel) => { (&mut channel.context, &channel.funding, channel.pending_splice.as_ref()) }, @@ -2372,7 +2398,7 @@ where ) -> Result<(Option>, Option), ChannelError> { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); match phase { - ChannelPhase::UnfundedV2(chan) => { + ChannelPhase::PendingV2(chan) => { let holder_commitment_point = match chan.unfunded_context.holder_commitment_point { Some(point) => point, None => { @@ -2483,6 +2509,9 @@ where ChannelPhase::UnfundedV2(chan) => { chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator) }, + ChannelPhase::PendingV2(chan) => { + chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator) + }, } } @@ -2527,6 +2556,15 @@ where } } +impl From> for Channel +where + SP::EcdsaSigner: ChannelSigner, +{ + fn from(channel: PendingV2Channel) -> Self { + Channel { phase: ChannelPhase::PendingV2(channel) } + } +} + impl From> for Channel where SP::EcdsaSigner: ChannelSigner, @@ -14283,6 +14321,20 @@ impl UnfundedV2Channel { } } +/// A V2 channel that has completed funding transaction construction but has not yet +/// received `commitment_signed`. +pub(super) struct PendingV2Channel { + pub funding: FundingScope, + pub context: ChannelContext, + pub unfunded_context: UnfundedChannelContext, +} + +impl PendingV2Channel { + pub fn abandon_unfunded_chan(&mut self, closure_reason: ClosureReason) -> ShutdownResult { + self.context.force_shutdown(&self.funding, closure_reason) + } +} + // Unfunded channel utilities pub(super) fn get_initial_channel_type( From 59f2edeee1370a651acb71e31fd4c9b1a673976a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 5 Mar 2026 15:37:14 -0600 Subject: [PATCH 4/4] Enforce ChannelTransactionParameters completeness at the type level ChannelTransactionParameters used Option fields for counterparty parameters and funding outpoint, relying on runtime unwrap/expect calls scattered throughout the codebase. This was fragile since it was easy to call code that assumed populated parameters on a not-yet-funded channel. Split into PartialChannelTransactionParameters (used during negotiation) and ChannelTransactionParameters (fully populated, used once funded). Make FundingScope generic over the parameters type so the compiler enforces which channel phases have complete data, eliminating the need for runtime is_populated() guards in signers and elsewhere. With the type-level distinction in place, also: - Remove the InitialRemoteCommitmentReceiver trait, replacing it with a ChannelContext method that takes complete parameters directly. - Replace unset_funding_info with Channel::unfund(), which properly transitions a funded channel back to unfunded state rather than leaving it in an inconsistent "funded without funding info" state. - Flatten DirectedChannelTransactionParameters to store resolved fields directly instead of computing them on each access. - Concretize methods that only work with complete parameters to take FundingScope directly, using field access instead of trait-based Option-returning accessors. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/chain/channelmonitor.rs | 31 +- lightning/src/chain/onchaintx.rs | 6 +- lightning/src/chain/package.rs | 10 +- lightning/src/ln/async_signer_tests.rs | 7 +- lightning/src/ln/chan_utils.rs | 273 +++-- lightning/src/ln/channel.rs | 1150 ++++++++++--------- lightning/src/ln/channel_state.rs | 27 +- lightning/src/ln/channelmanager.rs | 71 +- lightning/src/ln/functional_test_utils.rs | 2 +- lightning/src/ln/htlc_reserve_unit_tests.rs | 30 +- lightning/src/ln/payment_tests.rs | 14 +- lightning/src/ln/update_fee_tests.rs | 30 +- lightning/src/sign/mod.rs | 52 +- lightning/src/util/test_channel_signer.rs | 4 +- 14 files changed, 967 insertions(+), 740 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index a8d055a9c5b..91e9a6dda84 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -46,8 +46,8 @@ use crate::chain::{BestBlock, WatchedOutput}; use crate::events::bump_transaction::{AnchorDescriptor, BumpTransactionEvent}; use crate::events::{ClosureReason, Event, EventHandler, ReplayEvent}; use crate::ln::chan_utils::{ - self, ChannelTransactionParameters, CommitmentTransaction, CounterpartyCommitmentSecrets, - HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, + self, ChannelTransactionParameters, ChannelTransactionParametersAccess, CommitmentTransaction, + CounterpartyCommitmentSecrets, HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, }; use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; use crate::ln::channel_keys::{ @@ -118,9 +118,7 @@ impl ChannelMonitorUpdate { ) -> impl Iterator + '_ { self.updates.iter().filter_map(|update| match update { ChannelMonitorUpdateStep::RenegotiatedFunding { channel_parameters, .. } => { - let funding_outpoint = channel_parameters - .funding_outpoint - .expect("Renegotiated funding must always have known outpoint"); + let funding_outpoint = channel_parameters.funding_outpoint; let funding_script = channel_parameters.make_funding_redeemscript().to_p2wsh(); Some((funding_outpoint, funding_script)) }, @@ -1170,8 +1168,7 @@ struct FundingScope { impl FundingScope { fn funding_outpoint(&self) -> OutPoint { - let funding_outpoint = self.channel_parameters.funding_outpoint.as_ref(); - *funding_outpoint.expect("Funding outpoint must be set for active monitor") + self.channel_parameters.funding_outpoint } fn funding_txid(&self) -> Txid { @@ -1868,7 +1865,7 @@ impl ChannelMonitor { &channel_parameters.channel_type_features, &holder_pubkeys.payment_point ); - let counterparty_channel_parameters = channel_parameters.counterparty_parameters.as_ref().unwrap(); + let counterparty_channel_parameters = &channel_parameters.counterparty_parameters; let counterparty_delayed_payment_base_key = counterparty_channel_parameters.pubkeys.delayed_payment_basepoint; let counterparty_htlc_base_key = counterparty_channel_parameters.pubkeys.htlc_basepoint; let counterparty_commitment_params = CounterpartyCommitmentParameters { counterparty_delayed_payment_base_key, counterparty_htlc_base_key, on_counterparty_tx_csv }; @@ -1885,8 +1882,7 @@ impl ChannelMonitor { initial_holder_commitment_tx.clone(), secp_ctx, ); - let funding_outpoint = channel_parameters.funding_outpoint - .expect("Funding outpoint must be known during initialization"); + let funding_outpoint = channel_parameters.funding_outpoint; let funding_redeem_script = channel_parameters.make_funding_redeemscript(); let funding_script = funding_redeem_script.to_p2wsh(); let mut outputs_to_watch = new_hash_map(); @@ -4231,7 +4227,7 @@ impl ChannelMonitorImpl { channel_parameters, holder_commitment_tx, counterparty_commitment_tx, } => { log_trace!(logger, "Updating ChannelMonitor with alternative holder and counterparty commitment transactions for funding txid {}", - channel_parameters.funding_outpoint.unwrap().txid); + channel_parameters.funding_outpoint.txid); if let Err(_) = self.renegotiated_funding( logger, channel_parameters, holder_commitment_tx, counterparty_commitment_tx, ) { @@ -4345,7 +4341,6 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn get_funding_txo(&self) -> OutPoint { self.funding.channel_parameters.funding_outpoint - .expect("Funding outpoint must be set for active monitor") } /// Returns the P2WSH script we are currently monitoring the chain for spends. This will change @@ -6981,11 +6976,11 @@ mod tests { holder_pubkeys: keys.pubkeys(&secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys, selected_contest_delay: 67, - }), - funding_outpoint: Some(funding_outpoint), + }, + funding_outpoint, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::only_static_remote_key(), channel_value_satoshis: 0, @@ -7244,11 +7239,11 @@ mod tests { holder_pubkeys: keys.pubkeys(&secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys, selected_contest_delay: 67, - }), - funding_outpoint: Some(funding_outpoint), + }, + funding_outpoint, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::only_static_remote_key(), channel_value_satoshis: 0, diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 3eb6d64f3a2..512955fd16d 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1354,11 +1354,11 @@ mod tests { holder_pubkeys: signer.pubkeys(&secp_ctx), holder_selected_contest_delay: 66, is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys, selected_contest_delay: 67, - }), - funding_outpoint: Some(funding_outpoint), + }, + funding_outpoint, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::only_static_remote_key(), channel_value_satoshis: 0, diff --git a/lightning/src/chain/package.rs b/lightning/src/chain/package.rs index 0ef8855242b..773e6871cd8 100644 --- a/lightning/src/chain/package.rs +++ b/lightning/src/chain/package.rs @@ -31,8 +31,8 @@ use crate::chain::channelmonitor::COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE; use crate::chain::onchaintx::{FeerateStrategy, OnchainTxHandler}; use crate::chain::transaction::MaybeSignedTransaction; use crate::ln::chan_utils::{ - self, ChannelTransactionParameters, HTLCOutputInCommitment, HolderCommitmentTransaction, - TxCreationKeys, + self, ChannelTransactionParameters, ChannelTransactionParametersAccess, HTLCOutputInCommitment, + HolderCommitmentTransaction, TxCreationKeys, }; use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint}; use crate::ln::channelmanager::MIN_CLTV_EXPIRY_DELTA; @@ -1892,7 +1892,7 @@ mod tests { payment_hash: PaymentHash::from(preimage), transaction_output_index: None, }; - let funding_outpoint = channel_parameters.funding_outpoint.unwrap(); + let funding_outpoint = channel_parameters.funding_outpoint; let commitment_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); let trusted_tx = commitment_tx.trust(); PackageSolvingData::HolderHTLCOutput(HolderHTLCOutput::build( @@ -1929,7 +1929,7 @@ mod tests { payment_hash: PaymentHash::from(PaymentPreimage([2;32])), transaction_output_index: None, }; - let funding_outpoint = channel_parameters.funding_outpoint.unwrap(); + let funding_outpoint = channel_parameters.funding_outpoint; let commitment_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); let trusted_tx = commitment_tx.trust(); PackageSolvingData::HolderHTLCOutput(HolderHTLCOutput::build( @@ -1957,7 +1957,7 @@ mod tests { macro_rules! dumb_funding_output { () => {{ let mut channel_parameters = ChannelTransactionParameters::test_dummy(0); - let funding_outpoint = channel_parameters.funding_outpoint.unwrap(); + let funding_outpoint = channel_parameters.funding_outpoint; let commitment_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()); channel_parameters.channel_type_features = ChannelTypeFeatures::only_static_remote_key(); PackageSolvingData::HolderFundingOutput(HolderFundingOutput::build( diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index 451af3918bf..3f115328e0d 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -1244,12 +1244,11 @@ fn do_test_closing_signed(extra_closing_signed: bool, reconnect: bool) { let per_peer_state = nodes[1].node.per_peer_state.read().unwrap(); let mut chan_lock = per_peer_state.get(&node_a_id).unwrap().lock().unwrap(); let channel = chan_lock.channel_by_id.get_mut(&chan_id).unwrap(); - let (funding, context) = channel.funding_and_context_mut(); - - let signer = context.get_mut_signer().as_mut_ecdsa().unwrap(); + let funded = channel.as_funded_mut().unwrap(); + let signer = funded.context.get_mut_signer().as_mut_ecdsa().unwrap(); let signature = signer .sign_closing_transaction( - &funding.channel_transaction_parameters, + &funded.funding.channel_transaction_parameters, &closing_tx_2, &Secp256k1::new(), ) diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 4bb8ffac9ef..66e4f462b82 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -1040,13 +1040,49 @@ pub fn build_keyed_anchor_input_witness( ret } -/// Per-channel data used to build transactions in conjunction with the per-commitment data (CommitmentTransaction). -/// The fields are organized by holder/counterparty. +/// Trait providing unified access to channel transaction parameters fields shared by both +/// [`PartialChannelTransactionParameters`] and [`ChannelTransactionParameters`]. /// -/// Normally, this is converted to the broadcaster/countersignatory-organized DirectedChannelTransactionParameters -/// before use, via the as_holder_broadcastable and as_counterparty_broadcastable functions. +/// This enables generic code (e.g., `FundingScope

`) to access common fields without +/// knowing which concrete parameters type is in use. +pub(crate) trait ChannelTransactionParametersAccess { + fn holder_pubkeys(&self) -> &ChannelPublicKeys; + fn holder_selected_contest_delay(&self) -> u16; + fn is_outbound_from_holder(&self) -> bool; + fn channel_type_features(&self) -> &ChannelTypeFeatures; + fn channel_value_satoshis(&self) -> u64; + fn splice_parent_funding_txid(&self) -> Option; + /// Returns the funding outpoint, if known. + fn funding_outpoint(&self) -> Option; + /// Returns the counterparty parameters, if known. + fn counterparty_parameters(&self) -> Option<&CounterpartyChannelTransactionParameters>; + + /// Builds the funding redeemscript, if counterparty parameters are known. + fn make_funding_redeemscript_opt(&self) -> Option { + self.counterparty_parameters().map(|p| { + make_funding_redeemscript( + &self.holder_pubkeys().funding_pubkey, + &p.pubkeys.funding_pubkey, + ) + }) + } + + /// Builds the funding redeemscript, panicking if counterparty parameters are not yet known. + fn make_funding_redeemscript(&self) -> ScriptBuf { + self.make_funding_redeemscript_opt().unwrap() + } +} + +/// Per-channel data used to build transactions in conjunction with the per-commitment data +/// (CommitmentTransaction). The fields are organized by holder/counterparty. +/// +/// This is the internal variant used during channel negotiation, where counterparty parameters and +/// the funding outpoint may not yet be known. Once both are populated, this can be converted to +/// [`ChannelTransactionParameters`] via [`into_complete`]. +/// +/// [`into_complete`]: PartialChannelTransactionParameters::into_complete #[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct ChannelTransactionParameters { +pub(crate) struct PartialChannelTransactionParameters { /// Holder public keys pub holder_pubkeys: ChannelPublicKeys, /// The contest delay selected by the holder, which applies to counterparty-broadcast transactions @@ -1060,6 +1096,36 @@ pub struct ChannelTransactionParameters { /// The late-bound funding outpoint pub funding_outpoint: Option, /// The parent funding txid for a channel that has been spliced. + pub splice_parent_funding_txid: Option, + /// This channel's type, as negotiated during channel open. + pub channel_type_features: ChannelTypeFeatures, + /// The value locked in the channel, denominated in satoshis. + pub channel_value_satoshis: u64, +} + +/// Per-channel data used to build transactions in conjunction with the per-commitment data (CommitmentTransaction). +/// The fields are organized by holder/counterparty. +/// +/// Normally, this is converted to the broadcaster/countersignatory-organized DirectedChannelTransactionParameters +/// before use, via the as_holder_broadcastable and as_counterparty_broadcastable functions. +/// +/// All late-bound fields (counterparty parameters and funding outpoint) are guaranteed to be +/// present. For the partially-populated variant used during channel negotiation, see +/// [`PartialChannelTransactionParameters`]. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct ChannelTransactionParameters { + /// Holder public keys + pub holder_pubkeys: ChannelPublicKeys, + /// The contest delay selected by the holder, which applies to counterparty-broadcast transactions + pub holder_selected_contest_delay: u16, + /// Whether the holder is the initiator of this channel. + /// This is an input to the commitment number obscure factor computation. + pub is_outbound_from_holder: bool, + /// The counterparty channel transaction parameters. + pub counterparty_parameters: CounterpartyChannelTransactionParameters, + /// The funding outpoint + pub funding_outpoint: chain::transaction::OutPoint, + /// The parent funding txid for a channel that has been spliced. /// /// If a channel was funded with transaction A, and later spliced with transaction B, this field /// tracks the txid of transaction A. @@ -1086,54 +1152,119 @@ pub struct CounterpartyChannelTransactionParameters { pub selected_contest_delay: u16, } -impl ChannelTransactionParameters { - /// Whether the late bound parameters are populated. - pub fn is_populated(&self) -> bool { - self.counterparty_parameters.is_some() && self.funding_outpoint.is_some() +impl ChannelTransactionParametersAccess for PartialChannelTransactionParameters { + fn holder_pubkeys(&self) -> &ChannelPublicKeys { + &self.holder_pubkeys + } + fn holder_selected_contest_delay(&self) -> u16 { + self.holder_selected_contest_delay + } + fn is_outbound_from_holder(&self) -> bool { + self.is_outbound_from_holder + } + fn channel_type_features(&self) -> &ChannelTypeFeatures { + &self.channel_type_features + } + fn channel_value_satoshis(&self) -> u64 { + self.channel_value_satoshis + } + fn splice_parent_funding_txid(&self) -> Option { + self.splice_parent_funding_txid } + fn funding_outpoint(&self) -> Option { + self.funding_outpoint + } + fn counterparty_parameters(&self) -> Option<&CounterpartyChannelTransactionParameters> { + self.counterparty_parameters.as_ref() + } +} +impl ChannelTransactionParametersAccess for ChannelTransactionParameters { + fn holder_pubkeys(&self) -> &ChannelPublicKeys { + &self.holder_pubkeys + } + fn holder_selected_contest_delay(&self) -> u16 { + self.holder_selected_contest_delay + } + fn is_outbound_from_holder(&self) -> bool { + self.is_outbound_from_holder + } + fn channel_type_features(&self) -> &ChannelTypeFeatures { + &self.channel_type_features + } + fn channel_value_satoshis(&self) -> u64 { + self.channel_value_satoshis + } + fn splice_parent_funding_txid(&self) -> Option { + self.splice_parent_funding_txid + } + fn funding_outpoint(&self) -> Option { + Some(self.funding_outpoint) + } + fn counterparty_parameters(&self) -> Option<&CounterpartyChannelTransactionParameters> { + Some(&self.counterparty_parameters) + } +} + +impl PartialChannelTransactionParameters { + /// Converts to the complete type, returning `None` if late-bound fields are not populated. + pub(crate) fn into_complete(self) -> Option { + Some(ChannelTransactionParameters { + holder_pubkeys: self.holder_pubkeys, + holder_selected_contest_delay: self.holder_selected_contest_delay, + is_outbound_from_holder: self.is_outbound_from_holder, + counterparty_parameters: self.counterparty_parameters?, + funding_outpoint: self.funding_outpoint?, + splice_parent_funding_txid: self.splice_parent_funding_txid, + channel_type_features: self.channel_type_features, + channel_value_satoshis: self.channel_value_satoshis, + }) + } +} + +impl ChannelTransactionParameters { /// Convert the holder/counterparty parameters to broadcaster/countersignatory-organized parameters, /// given that the holder is the broadcaster. - /// - /// self.is_populated() must be true before calling this function. #[rustfmt::skip] pub fn as_holder_broadcastable(&self) -> DirectedChannelTransactionParameters<'_> { - assert!(self.is_populated(), "self.late_parameters must be set before using as_holder_broadcastable"); DirectedChannelTransactionParameters { - inner: self, - holder_is_broadcaster: true + broadcaster_pubkeys: &self.holder_pubkeys, + countersignatory_pubkeys: &self.counterparty_parameters.pubkeys, + contest_delay: self.counterparty_parameters.selected_contest_delay, + is_outbound: self.is_outbound_from_holder, + funding_outpoint: self.funding_outpoint.into_bitcoin_outpoint(), + channel_type_features: &self.channel_type_features, + channel_value_satoshis: self.channel_value_satoshis, } } /// Convert the holder/counterparty parameters to broadcaster/countersignatory-organized parameters, /// given that the counterparty is the broadcaster. - /// - /// self.is_populated() must be true before calling this function. #[rustfmt::skip] pub fn as_counterparty_broadcastable(&self) -> DirectedChannelTransactionParameters<'_> { - assert!(self.is_populated(), "self.late_parameters must be set before using as_counterparty_broadcastable"); DirectedChannelTransactionParameters { - inner: self, - holder_is_broadcaster: false + broadcaster_pubkeys: &self.counterparty_parameters.pubkeys, + countersignatory_pubkeys: &self.holder_pubkeys, + contest_delay: self.holder_selected_contest_delay, + is_outbound: !self.is_outbound_from_holder, + funding_outpoint: self.funding_outpoint.into_bitcoin_outpoint(), + channel_type_features: &self.channel_type_features, + channel_value_satoshis: self.channel_value_satoshis, } } - pub(crate) fn make_funding_redeemscript(&self) -> ScriptBuf { - self.make_funding_redeemscript_opt().unwrap() - } - - pub(crate) fn make_funding_redeemscript_opt(&self) -> Option { - self.counterparty_parameters.as_ref().map(|p| { - make_funding_redeemscript( - &self.holder_pubkeys.funding_pubkey, - &p.pubkeys.funding_pubkey, - ) - }) - } - - /// Returns the counterparty's pubkeys. - pub fn counterparty_pubkeys(&self) -> Option<&ChannelPublicKeys> { - self.counterparty_parameters.as_ref().map(|params| ¶ms.pubkeys) + /// Converts to the partial type (with Options). + pub(crate) fn into_partial(self) -> PartialChannelTransactionParameters { + PartialChannelTransactionParameters { + holder_pubkeys: self.holder_pubkeys, + holder_selected_contest_delay: self.holder_selected_contest_delay, + is_outbound_from_holder: self.is_outbound_from_holder, + counterparty_parameters: Some(self.counterparty_parameters), + funding_outpoint: Some(self.funding_outpoint), + splice_parent_funding_txid: self.splice_parent_funding_txid, + channel_type_features: self.channel_type_features, + channel_value_satoshis: self.channel_value_satoshis, + } } #[cfg(test)] @@ -1150,13 +1281,13 @@ impl ChannelTransactionParameters { holder_pubkeys: dummy_keys.clone(), holder_selected_contest_delay: 42, is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: dummy_keys, selected_contest_delay: 42, - }), - funding_outpoint: Some(chain::transaction::OutPoint { + }, + funding_outpoint: chain::transaction::OutPoint { txid: Txid::from_byte_array([42; 32]), index: 0 - }), + }, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::empty(), channel_value_satoshis, @@ -1173,12 +1304,14 @@ impl Writeable for ChannelTransactionParameters { #[rustfmt::skip] fn write(&self, writer: &mut W) -> Result<(), io::Error> { let legacy_deserialization_prevention_marker = legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features); + let counterparty_parameters = Some(&self.counterparty_parameters); + let funding_outpoint = Some(&self.funding_outpoint); write_tlv_fields!(writer, { (0, self.holder_pubkeys, required), (2, self.holder_selected_contest_delay, required), (4, self.is_outbound_from_holder, required), - (6, self.counterparty_parameters, option), - (8, self.funding_outpoint, option), + (6, counterparty_parameters, option), + (8, funding_outpoint, option), (10, legacy_deserialization_prevention_marker, option), (11, self.channel_type_features, required), (12, self.splice_parent_funding_txid, option), @@ -1189,6 +1322,13 @@ impl Writeable for ChannelTransactionParameters { } impl ReadableArgs> for ChannelTransactionParameters { + fn read(reader: &mut R, read_args: Option) -> Result { + let params = PartialChannelTransactionParameters::read(reader, read_args)?; + params.into_complete().ok_or(DecodeError::InvalidValue) + } +} + +impl ReadableArgs> for PartialChannelTransactionParameters { #[rustfmt::skip] fn read(reader: &mut R, read_args: Option) -> Result { let mut holder_pubkeys = RequiredWrapper(None); @@ -1248,61 +1388,60 @@ impl ReadableArgs> for ChannelTransactionParameters { /// This is derived from the holder/counterparty-organized ChannelTransactionParameters via the /// as_holder_broadcastable and as_counterparty_broadcastable functions. pub struct DirectedChannelTransactionParameters<'a> { - /// The holder's channel static parameters - inner: &'a ChannelTransactionParameters, - /// Whether the holder is the broadcaster - holder_is_broadcaster: bool, + /// The channel pubkeys for the broadcaster + broadcaster_pubkeys: &'a ChannelPublicKeys, + /// The channel pubkeys for the countersignatory + countersignatory_pubkeys: &'a ChannelPublicKeys, + /// The contest delay selected by the countersignatory + contest_delay: u16, + /// Whether the channel is outbound from the broadcaster + is_outbound: bool, + /// The funding outpoint + funding_outpoint: OutPoint, + /// The channel type features + channel_type_features: &'a ChannelTypeFeatures, + /// The value locked in the channel + channel_value_satoshis: u64, } impl<'a> DirectedChannelTransactionParameters<'a> { /// Get the channel pubkeys for the broadcaster pub fn broadcaster_pubkeys(&self) -> &'a ChannelPublicKeys { - if self.holder_is_broadcaster { - &self.inner.holder_pubkeys - } else { - &self.inner.counterparty_parameters.as_ref().unwrap().pubkeys - } + self.broadcaster_pubkeys } /// Get the channel pubkeys for the countersignatory pub fn countersignatory_pubkeys(&self) -> &'a ChannelPublicKeys { - if self.holder_is_broadcaster { - &self.inner.counterparty_parameters.as_ref().unwrap().pubkeys - } else { - &self.inner.holder_pubkeys - } + self.countersignatory_pubkeys } /// Get the contest delay applicable to the transactions. /// Note that the contest delay was selected by the countersignatory. - #[rustfmt::skip] pub fn contest_delay(&self) -> u16 { - let counterparty_parameters = self.inner.counterparty_parameters.as_ref().unwrap(); - if self.holder_is_broadcaster { counterparty_parameters.selected_contest_delay } else { self.inner.holder_selected_contest_delay } + self.contest_delay } /// Whether the channel is outbound from the broadcaster. /// /// The boolean representing the side that initiated the channel is /// an input to the commitment number obscure factor computation. - #[rustfmt::skip] pub fn is_outbound(&self) -> bool { - if self.holder_is_broadcaster { self.inner.is_outbound_from_holder } else { !self.inner.is_outbound_from_holder } + self.is_outbound } /// The funding outpoint pub fn funding_outpoint(&self) -> OutPoint { - self.inner.funding_outpoint.unwrap().into_bitcoin_outpoint() + self.funding_outpoint } /// The type of channel these parameters are for pub fn channel_type_features(&self) -> &'a ChannelTypeFeatures { - &self.inner.channel_type_features + self.channel_type_features } /// The value locked in the channel, denominated in satoshis. pub fn channel_value_satoshis(&self) -> u64 { - self.inner.channel_value_satoshis + self.channel_value_satoshis } } @@ -1362,8 +1501,8 @@ impl HolderCommitmentTransaction { holder_pubkeys: channel_pubkeys.clone(), holder_selected_contest_delay: 0, is_outbound_from_holder: false, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: channel_pubkeys.clone(), selected_contest_delay: 0 }), - funding_outpoint: Some(funding_outpoint), + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: channel_pubkeys.clone(), selected_contest_delay: 0 }, + funding_outpoint: funding_outpoint, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::only_static_remote_key(), channel_value_satoshis, @@ -2299,8 +2438,8 @@ mod tests { holder_pubkeys: holder_pubkeys.clone(), holder_selected_contest_delay: 0, is_outbound_from_holder: false, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys.clone(), selected_contest_delay: 0 }), - funding_outpoint: Some(chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }), + counterparty_parameters: CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys.clone(), selected_contest_delay: 0 }, + funding_outpoint: chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }, splice_parent_funding_txid: None, channel_type_features: ChannelTypeFeatures::only_static_remote_key(), channel_value_satoshis: 4000, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4a8dc93fbd8..657cf2229ea 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -41,8 +41,9 @@ use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat, selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters, - ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters, - CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction, + ChannelTransactionParametersAccess, ClosingTransaction, CommitmentTransaction, + CounterpartyChannelTransactionParameters, CounterpartyCommitmentSecrets, + HTLCOutputInCommitment, HolderCommitmentTransaction, PartialChannelTransactionParameters, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, }; use crate::ln::channel_state::{ @@ -1523,40 +1524,91 @@ where } } - pub fn funding(&self) -> &FundingScope { + pub fn is_outbound(&self) -> bool { match &self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::Funded(chan) => &chan.funding, - ChannelPhase::UnfundedOutboundV1(chan) => &chan.funding, - ChannelPhase::PendingV1(chan) => &chan.funding, - ChannelPhase::UnfundedInboundV1(chan) => &chan.funding, - ChannelPhase::UnfundedV2(chan) => &chan.funding, - ChannelPhase::PendingV2(chan) => &chan.funding, + ChannelPhase::Funded(chan) => chan.funding.is_outbound(), + ChannelPhase::UnfundedOutboundV1(chan) => chan.funding.is_outbound(), + ChannelPhase::PendingV1(chan) => chan.funding.is_outbound(), + ChannelPhase::UnfundedInboundV1(chan) => chan.funding.is_outbound(), + ChannelPhase::UnfundedV2(chan) => chan.funding.is_outbound(), + ChannelPhase::PendingV2(chan) => chan.funding.is_outbound(), } } - #[cfg(any(test, feature = "_externalize_tests"))] - pub fn funding_mut(&mut self) -> &mut FundingScope { - match &mut self.phase { + pub fn get_channel_type(&self) -> &ChannelTypeFeatures { + match &self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::Funded(chan) => &mut chan.funding, - ChannelPhase::UnfundedOutboundV1(chan) => &mut chan.funding, - ChannelPhase::PendingV1(chan) => &mut chan.funding, - ChannelPhase::UnfundedInboundV1(chan) => &mut chan.funding, - ChannelPhase::UnfundedV2(chan) => &mut chan.funding, - ChannelPhase::PendingV2(chan) => &mut chan.funding, + ChannelPhase::Funded(chan) => chan.funding.get_channel_type(), + ChannelPhase::UnfundedOutboundV1(chan) => chan.funding.get_channel_type(), + ChannelPhase::PendingV1(chan) => chan.funding.get_channel_type(), + ChannelPhase::UnfundedInboundV1(chan) => chan.funding.get_channel_type(), + ChannelPhase::UnfundedV2(chan) => chan.funding.get_channel_type(), + ChannelPhase::PendingV2(chan) => chan.funding.get_channel_type(), } } - pub fn funding_and_context_mut(&mut self) -> (&FundingScope, &mut ChannelContext) { - match &mut self.phase { + pub fn get_funding_txo(&self) -> Option { + match &self.phase { + ChannelPhase::Undefined => unreachable!(), + ChannelPhase::Funded(chan) => chan.funding.get_funding_txo(), + ChannelPhase::UnfundedOutboundV1(chan) => chan.funding.get_funding_txo(), + ChannelPhase::PendingV1(chan) => chan.funding.get_funding_txo(), + ChannelPhase::UnfundedInboundV1(chan) => chan.funding.get_funding_txo(), + ChannelPhase::UnfundedV2(chan) => chan.funding.get_funding_txo(), + ChannelPhase::PendingV2(chan) => chan.funding.get_funding_txo(), + } + } + + #[cfg(test)] + pub fn get_short_channel_id(&self) -> Option { + match &self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::Funded(chan) => (&chan.funding, &mut chan.context), - ChannelPhase::UnfundedOutboundV1(chan) => (&chan.funding, &mut chan.context), - ChannelPhase::PendingV1(chan) => (&chan.funding, &mut chan.context), - ChannelPhase::UnfundedInboundV1(chan) => (&chan.funding, &mut chan.context), - ChannelPhase::UnfundedV2(chan) => (&chan.funding, &mut chan.context), - ChannelPhase::PendingV2(chan) => (&chan.funding, &mut chan.context), + ChannelPhase::Funded(chan) => chan.funding.get_short_channel_id(), + ChannelPhase::UnfundedOutboundV1(chan) => chan.funding.get_short_channel_id(), + ChannelPhase::PendingV1(chan) => chan.funding.get_short_channel_id(), + ChannelPhase::UnfundedInboundV1(chan) => chan.funding.get_short_channel_id(), + ChannelPhase::UnfundedV2(chan) => chan.funding.get_short_channel_id(), + ChannelPhase::PendingV2(chan) => chan.funding.get_short_channel_id(), + } + } + + #[rustfmt::skip] + pub(super) fn channel_details( + &self, best_block_height: u32, latest_features: InitFeatures, + fee_estimator: &LowerBoundedFeeEstimator, + ) -> super::channel_state::ChannelDetails { + let context = self.context(); + let balance = self.get_available_balances(fee_estimator).unwrap_or_else(|()| { + debug_assert!(false, "some channel balance has been overdrawn"); + AvailableBalances { + inbound_capacity_msat: 0, + outbound_capacity_msat: 0, + next_outbound_htlc_limit_msat: 0, + next_outbound_htlc_minimum_msat: u64::MAX, + } + }); + let minimum_depth = self.minimum_depth(); + match &self.phase { + ChannelPhase::Undefined => unreachable!(), + ChannelPhase::Funded(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), + ChannelPhase::UnfundedOutboundV1(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), + ChannelPhase::PendingV1(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), + ChannelPhase::UnfundedInboundV1(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), + ChannelPhase::UnfundedV2(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), + ChannelPhase::PendingV2(chan) => super::channel_state::ChannelDetails::from_channel_parts( + context, &chan.funding, balance, minimum_depth, best_block_height, latest_features, fee_estimator, + ), } } @@ -1603,6 +1655,15 @@ where } } + #[cfg(any(test, feature = "_externalize_tests"))] + pub fn as_unfunded_inbound_v1_mut(&mut self) -> Option<&mut InboundV1Channel> { + if let ChannelPhase::UnfundedInboundV1(channel) = &mut self.phase { + Some(channel) + } else { + None + } + } + #[cfg(any(test, feature = "_externalize_tests"))] pub fn is_unfunded_v1(&self) -> bool { matches!( @@ -1617,7 +1678,7 @@ where /// /// If this method returns true, [`Self::into_unfunded_outbound_v1`] will also succeed. pub fn ready_to_fund(&self) -> bool { - if !self.funding().is_outbound() { + if !self.is_outbound() { return false; } match self.context().channel_state { @@ -1734,10 +1795,11 @@ where // handshake (and bailing if the peer rejects it), so we force-close in // that case. ChannelPhase::UnfundedOutboundV1(chan) => chan.is_resumable(), + // PendingV1 has committed to a funding transaction so we don't yet + // support replaying the funding handshake on reconnection. ChannelPhase::PendingV1(_) => false, ChannelPhase::UnfundedInboundV1(_) => false, - ChannelPhase::UnfundedV2(_) => false, - ChannelPhase::PendingV2(_) => false, + ChannelPhase::UnfundedV2(_) | ChannelPhase::PendingV2(_) => false, }; let splice_funding_failed = if let ChannelPhase::Funded(chan) = &mut self.phase { @@ -1785,8 +1847,15 @@ where .map(|msg| ReconnectionMsg::Open(OpenChannelMessage::V1(msg))) .unwrap_or(ReconnectionMsg::None) }, - ChannelPhase::PendingV1(_) | ChannelPhase::PendingV2(_) => { - // Pending channels are not resumable, so this shouldn't be reached. + ChannelPhase::PendingV1(_) => { + // PendingV1 channels are not resumable, so this shouldn't be reached. + debug_assert!(false); + ReconnectionMsg::None + }, + ChannelPhase::UnfundedInboundV1(_) => { + // Since unfunded inbound channel maps are cleared upon disconnecting a peer, + // they are not persisted and won't be recovered after a crash. + // Therefore, they shouldn't exist at this point. debug_assert!(false); ReconnectionMsg::None }, @@ -1803,10 +1872,8 @@ where ReconnectionMsg::None } }, - ChannelPhase::UnfundedInboundV1(_) => { - // Since unfunded inbound channel maps are cleared upon disconnecting a peer, - // they are not persisted and won't be recovered after a crash. - // Therefore, they shouldn't exist at this point. + ChannelPhase::PendingV2(_) => { + // PendingV2 channels are not resumable, so this shouldn't be reached. debug_assert!(false); ReconnectionMsg::None }, @@ -2039,15 +2106,15 @@ where let err = "Got an unexpected tx_abort message: This is an unfunded channel created with V1 channel establishment"; return Err(ChannelError::Warn(err.into())); }, - ChannelPhase::PendingV2(chan) => { - let had_session = chan.context.interactive_tx_signing_session.take().is_some(); - (had_session, None, false) - }, ChannelPhase::UnfundedV2(pending_v2_channel) => { let had_constructor = pending_v2_channel.interactive_tx_constructor.take().is_some(); (had_constructor, None, false) }, + ChannelPhase::PendingV2(chan) => { + let had_session = chan.context.interactive_tx_signing_session.take().is_some(); + (had_session, None, false) + }, ChannelPhase::Funded(funded_channel) => { if funded_channel.has_pending_splice_awaiting_signatures() && funded_channel @@ -2126,6 +2193,40 @@ where result.map(|monitor| (self.as_funded_mut().expect("Channel should be funded"), monitor)) } + /// Reverts a channel from `Funded` back to `UnfundedOutboundV1`, clearing the funding + /// outpoint. Used when `watch_channel` fails after `funding_signed` to ensure + /// `force_shutdown` doesn't generate a monitor update for an unregistered channel. + /// + /// The channel must be immediately shut down after this call. + pub(super) fn unfund(&mut self) { + let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); + self.phase = if let ChannelPhase::Funded(chan) = phase { + debug_assert!(matches!( + chan.context.channel_state, + ChannelState::AwaitingChannelReady(_) + )); + let mut funding = chan.funding.into_partial(); + funding.channel_transaction_parameters.funding_outpoint = None; + let channel_id = chan.context.temporary_channel_id.expect( + "temporary_channel_id should be set since unfund is only called on \ + channels that were unfunded immediately beforehand", + ); + let mut context = chan.context; + context.channel_id = channel_id; + ChannelPhase::UnfundedOutboundV1(OutboundV1Channel { + funding, + context, + unfunded_context: UnfundedChannelContext { + unfunded_channel_age_ticks: 0, + holder_commitment_point: Some(chan.holder_commitment_point), + }, + signer_pending_open_channel: false, + }) + } else { + panic!("unfund called on non-Funded channel"); + }; + } + fn funding_tx_constructed(&mut self, funding_outpoint: OutPoint) -> Result<(), AbortReason> { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); let result = match phase { @@ -2151,12 +2252,16 @@ where .interactive_tx_constructor .take() .expect("UnfundedV2Channel::interactive_tx_constructor should be set"); + let funding = chan + .funding + .into_complete() + .expect("funding params must be complete after tx construction"); let signing_session = interactive_tx_constructor.into_signing_session(); chan.context.interactive_tx_signing_session = Some(signing_session); self.phase = ChannelPhase::PendingV2(PendingV2Channel { - funding: chan.funding, + funding, context: chan.context, unfunded_context: chan.unfunded_context, }); @@ -2173,6 +2278,9 @@ where let is_initiator = interactive_tx_constructor.is_initiator(); funding.channel_transaction_parameters.funding_outpoint = Some(funding_outpoint); + let funding = funding + .into_complete() + .expect("funding params must be complete after tx construction"); pending_splice.funding_negotiation = Some(FundingNegotiation::AwaitingSignatures { is_initiator, @@ -2259,7 +2367,7 @@ where signing_session } else { - if Some(funding_txid_signed) == funding.get_funding_txid() { + if funding_txid_signed == funding.get_funding_txid() { // We may be handling a duplicate call and the funding was already locked so we // no longer have the signing session present. return Ok(FundingTxSigned { @@ -2318,12 +2426,15 @@ where ); } - let funding = pending_splice + let commitment_signed = if let Some(splice_funding) = pending_splice .as_ref() .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) .and_then(|funding_negotiation| funding_negotiation.as_funding()) - .unwrap_or(funding); - let commitment_signed = context.get_initial_commitment_signed_v2(funding, &&logger); + { + context.get_initial_commitment_signed_v2(splice_funding, &&logger) + } else { + context.get_initial_commitment_signed_v2(funding, &&logger) + }; // For zero conf channels, we don't expect the funding transaction to be ready for broadcast // yet as, according to the spec, our counterparty shouldn't have sent their `tx_signatures` @@ -2388,8 +2499,27 @@ where } pub fn force_shutdown(&mut self, closure_reason: ClosureReason) -> ShutdownResult { - let (funding, context) = self.funding_and_context_mut(); - context.force_shutdown(funding, closure_reason) + match &mut self.phase { + ChannelPhase::Undefined => unreachable!(), + ChannelPhase::Funded(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + ChannelPhase::UnfundedOutboundV1(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + ChannelPhase::PendingV1(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + ChannelPhase::UnfundedInboundV1(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + ChannelPhase::UnfundedV2(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + ChannelPhase::PendingV2(chan) => { + chan.context.force_shutdown(&chan.funding, closure_reason) + }, + } } #[rustfmt::skip] @@ -2516,7 +2646,15 @@ where } pub fn minimum_depth(&self) -> Option { - self.context().minimum_depth(self.funding()) + match &self.phase { + ChannelPhase::Undefined => unreachable!(), + ChannelPhase::Funded(chan) => chan.context.minimum_depth(&chan.funding), + ChannelPhase::UnfundedOutboundV1(chan) => chan.context.minimum_depth(&chan.funding), + ChannelPhase::PendingV1(chan) => chan.context.minimum_depth(&chan.funding), + ChannelPhase::UnfundedInboundV1(chan) => chan.context.minimum_depth(&chan.funding), + ChannelPhase::UnfundedV2(chan) => chan.context.minimum_depth(&chan.funding), + ChannelPhase::PendingV2(chan) => chan.context.minimum_depth(&chan.funding), + } } } @@ -2609,7 +2747,7 @@ impl UnfundedChannelContext { /// during channel establishment and may be replaced during channel splicing or if the attempted /// funding transaction is replaced using tx_init_rbf. #[derive(Debug)] -pub(super) struct FundingScope { +pub(super) struct FundingScope { value_to_self_msat: u64, // Excluding all pending_htlcs, fees, and anchor outputs /// minimum channel reserve for self to maintain - set by them. @@ -2635,7 +2773,7 @@ pub(super) struct FundingScope { #[cfg(any(test, fuzzing))] next_remote_fee: Mutex, - pub(super) channel_transaction_parameters: ChannelTransactionParameters, + pub(super) channel_transaction_parameters: P, /// The transaction which funds this channel. Note that for manually-funded channels (i.e., /// [`ChannelContext::is_manual_broadcast`] is true) this will be a dummy empty transaction. @@ -2650,7 +2788,7 @@ pub(super) struct FundingScope { minimum_depth_override: Option, } -impl Writeable for FundingScope { +impl Writeable for FundingScope { fn write(&self, writer: &mut W) -> Result<(), io::Error> { write_tlv_fields!(writer, { (1, self.value_to_self_msat, required), @@ -2667,13 +2805,20 @@ impl Writeable for FundingScope { } } -impl Readable for FundingScope { +impl Readable for FundingScope { + fn read(reader: &mut R) -> Result { + let funding = FundingScope::::read(reader)?; + funding.into_complete().ok_or(DecodeError::InvalidValue) + } +} + +impl Readable for FundingScope { #[rustfmt::skip] fn read(reader: &mut R) -> Result { let mut value_to_self_msat = RequiredWrapper(None); let mut counterparty_selected_channel_reserve_satoshis = None; let mut holder_selected_channel_reserve_satoshis = RequiredWrapper(None); - let mut channel_transaction_parameters = RequiredWrapper(None); + let mut channel_transaction_parameters: RequiredWrapper = RequiredWrapper(None); let mut funding_transaction = None; let mut funding_tx_confirmed_in = None; let mut funding_tx_confirmation_height = RequiredWrapper(None); @@ -2684,7 +2829,7 @@ impl Readable for FundingScope { (1, value_to_self_msat, required), (3, counterparty_selected_channel_reserve_satoshis, option), (5, holder_selected_channel_reserve_satoshis, required), - (7, channel_transaction_parameters, (required: ReadableArgs, None)), + (7, channel_transaction_parameters, (required: ReadableArgs, None::)), (9, funding_transaction, option), (11, funding_tx_confirmed_in, option), (13, funding_tx_confirmation_height, required), @@ -2706,6 +2851,7 @@ impl Readable for FundingScope { funding_tx_confirmation_height: funding_tx_confirmation_height.0.unwrap(), short_channel_id, minimum_depth_override, + #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), #[cfg(any(test, fuzzing))] @@ -2714,9 +2860,9 @@ impl Readable for FundingScope { } } -impl FundingScope { +impl FundingScope

{ pub fn get_value_satoshis(&self) -> u64 { - self.channel_transaction_parameters.channel_value_satoshis + self.channel_transaction_parameters.channel_value_satoshis() } pub(crate) fn get_value_to_self_msat(&self) -> u64 { @@ -2741,13 +2887,13 @@ impl FundingScope { } pub fn is_outbound(&self) -> bool { - self.channel_transaction_parameters.is_outbound_from_holder + self.channel_transaction_parameters.is_outbound_from_holder() } /// Returns the funding_txo we either got from our peer, or were given by /// get_funding_created. pub fn get_funding_txo(&self) -> Option { - self.channel_transaction_parameters.funding_outpoint + self.channel_transaction_parameters.funding_outpoint() } /// Gets the funding output for this channel, if available. @@ -2764,25 +2910,26 @@ impl FundingScope { }) } - fn get_funding_txid(&self) -> Option { - self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) - } - fn get_holder_selected_contest_delay(&self) -> u16 { - self.channel_transaction_parameters.holder_selected_contest_delay + self.channel_transaction_parameters.holder_selected_contest_delay() } fn get_holder_pubkeys(&self) -> &ChannelPublicKeys { - &self.channel_transaction_parameters.holder_pubkeys + self.channel_transaction_parameters.holder_pubkeys() } pub fn get_counterparty_selected_contest_delay(&self) -> Option { - let params_opt = self.channel_transaction_parameters.counterparty_parameters.as_ref(); - params_opt.map(|params| params.selected_contest_delay) + self.channel_transaction_parameters + .counterparty_parameters() + .map(|params| params.selected_contest_delay) } fn get_counterparty_pubkeys(&self) -> &ChannelPublicKeys { - &self.channel_transaction_parameters.counterparty_parameters.as_ref().unwrap().pubkeys + &self + .channel_transaction_parameters + .counterparty_parameters() + .expect("counterparty_parameters must be set") + .pubkeys } /// Gets the redeemscript for the funding transaction output (ie the funding transaction output @@ -2802,7 +2949,7 @@ impl FundingScope { /// Gets the channel's type pub fn get_channel_type(&self) -> &ChannelTypeFeatures { - &self.channel_transaction_parameters.channel_type_features + self.channel_transaction_parameters.channel_type_features() } /// Returns the height in which our funding transaction was confirmed. @@ -2831,12 +2978,40 @@ impl FundingScope { pub fn get_short_channel_id(&self) -> Option { self.short_channel_id } +} + +impl FundingScope { + /// Converts this partial `FundingScope` into a `FundingScope` with complete parameters. + /// + /// Returns `None` if the channel transaction parameters are not fully populated. + fn into_complete(self) -> Option> { + Some(FundingScope { + channel_transaction_parameters: self.channel_transaction_parameters.into_complete()?, + value_to_self_msat: self.value_to_self_msat, + counterparty_selected_channel_reserve_satoshis: self + .counterparty_selected_channel_reserve_satoshis, + holder_selected_channel_reserve_satoshis: self.holder_selected_channel_reserve_satoshis, + #[cfg(debug_assertions)] + holder_prev_commitment_tx_balance: self.holder_prev_commitment_tx_balance, + #[cfg(debug_assertions)] + counterparty_prev_commitment_tx_balance: self.counterparty_prev_commitment_tx_balance, + #[cfg(any(test, fuzzing))] + next_local_fee: self.next_local_fee, + #[cfg(any(test, fuzzing))] + next_remote_fee: self.next_remote_fee, + funding_transaction: self.funding_transaction, + funding_tx_confirmed_in: self.funding_tx_confirmed_in, + funding_tx_confirmation_height: self.funding_tx_confirmation_height, + short_channel_id: self.short_channel_id, + minimum_depth_override: self.minimum_depth_override, + }) + } /// Constructs a `FundingScope` for splicing a channel. fn for_splice( - prev_funding: &Self, context: &ChannelContext, our_funding_contribution: SignedAmount, - their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, - our_new_holder_keys: ChannelPublicKeys, + prev_funding: &FundingScope, context: &ChannelContext, + our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, + counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys, ) -> Self { debug_assert!(our_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); debug_assert!(their_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); @@ -2853,15 +3028,15 @@ impl FundingScope { let post_value_to_self_msat = post_value_to_self_msat.unwrap(); let channel_parameters = &prev_funding.channel_transaction_parameters; - let mut post_channel_transaction_parameters = ChannelTransactionParameters { + let mut post_channel_transaction_parameters = PartialChannelTransactionParameters { holder_pubkeys: our_new_holder_keys, - holder_selected_contest_delay: channel_parameters.holder_selected_contest_delay, + holder_selected_contest_delay: channel_parameters.holder_selected_contest_delay(), // The 'outbound' attribute doesn't change, even if the splice initiator is the other node - is_outbound_from_holder: channel_parameters.is_outbound_from_holder, - counterparty_parameters: channel_parameters.counterparty_parameters.clone(), + is_outbound_from_holder: channel_parameters.is_outbound_from_holder(), + counterparty_parameters: channel_parameters.counterparty_parameters().cloned(), funding_outpoint: None, // filled later - splice_parent_funding_txid: prev_funding.get_funding_txid(), - channel_type_features: channel_parameters.channel_type_features.clone(), + splice_parent_funding_txid: Some(prev_funding.get_funding_txid()), + channel_type_features: channel_parameters.channel_type_features().clone(), channel_value_satoshis: post_channel_value, }; post_channel_transaction_parameters @@ -2927,6 +3102,37 @@ impl FundingScope { short_channel_id: None, } } +} + +impl FundingScope { + fn get_funding_txid(&self) -> Txid { + self.channel_transaction_parameters.funding_outpoint.txid + } + + /// Converts this `FundingScope` with complete parameters back into one with partial + /// parameters. Used when reverting a channel from funded to unfunded state. + fn into_partial(self) -> FundingScope { + FundingScope { + channel_transaction_parameters: self.channel_transaction_parameters.into_partial(), + value_to_self_msat: self.value_to_self_msat, + counterparty_selected_channel_reserve_satoshis: self + .counterparty_selected_channel_reserve_satoshis, + holder_selected_channel_reserve_satoshis: self.holder_selected_channel_reserve_satoshis, + #[cfg(debug_assertions)] + holder_prev_commitment_tx_balance: self.holder_prev_commitment_tx_balance, + #[cfg(debug_assertions)] + counterparty_prev_commitment_tx_balance: self.counterparty_prev_commitment_tx_balance, + #[cfg(any(test, fuzzing))] + next_local_fee: self.next_local_fee, + #[cfg(any(test, fuzzing))] + next_remote_fee: self.next_remote_fee, + funding_transaction: self.funding_transaction, + funding_tx_confirmed_in: self.funding_tx_confirmed_in, + funding_tx_confirmation_height: self.funding_tx_confirmation_height, + short_channel_id: self.short_channel_id, + minimum_depth_override: self.minimum_depth_override, + } + } /// Compute the post-splice channel value from each counterparty's contributions. pub(super) fn compute_post_splice_value( @@ -2975,7 +3181,7 @@ struct PendingFunding { /// Funding candidates that have been negotiated but have not reached enough confirmations /// by both counterparties to have exchanged `splice_locked` and be promoted. - negotiated_candidates: Vec, + negotiated_candidates: Vec>, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option, @@ -2998,11 +3204,11 @@ enum FundingNegotiation { new_holder_funding_key: PublicKey, }, ConstructingTransaction { - funding: FundingScope, + funding: FundingScope, interactive_tx_constructor: InteractiveTxConstructor, }, AwaitingSignatures { - funding: FundingScope, + funding: FundingScope, is_initiator: bool, /// The initial [`msgs::CommitmentSigned`] message received for the [`FundingScope`] above. /// We delay processing this until the user manually approves the splice via @@ -3028,11 +3234,35 @@ impl_writeable_tlv_based_enum_upgradable!(FundingNegotiation, ); impl FundingNegotiation { - fn as_funding(&self) -> Option<&FundingScope> { + /// Returns the funding from `AwaitingSignatures`, which has complete channel parameters. + fn as_funding(&self) -> Option<&FundingScope> { match self { - FundingNegotiation::AwaitingAck { .. } => None, - FundingNegotiation::ConstructingTransaction { funding, .. } => Some(funding), FundingNegotiation::AwaitingSignatures { funding, .. } => Some(funding), + _ => None, + } + } + + /// Returns the funding txo from whichever negotiation state has funding. + fn get_funding_txo(&self) -> Option { + match self { + FundingNegotiation::AwaitingAck { .. } => None, + FundingNegotiation::ConstructingTransaction { funding, .. } => { + funding.get_funding_txo() + }, + FundingNegotiation::AwaitingSignatures { funding, .. } => funding.get_funding_txo(), + } + } + + /// Returns the channel type from whichever negotiation state has funding. + fn get_channel_type(&self) -> Option<&ChannelTypeFeatures> { + match self { + FundingNegotiation::AwaitingAck { .. } => None, + FundingNegotiation::ConstructingTransaction { funding, .. } => { + Some(funding.get_channel_type()) + }, + FundingNegotiation::AwaitingSignatures { funding, .. } => { + Some(funding.get_channel_type()) + }, } } @@ -3058,13 +3288,7 @@ impl PendingFunding { return None; } - let confirmed_funding_txid = match funding.get_funding_txid() { - Some(funding_txid) => funding_txid, - None => { - debug_assert!(false); - return None; - }, - }; + let confirmed_funding_txid = funding.get_funding_txid(); match self.sent_funding_txid { Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => None, @@ -3434,221 +3658,6 @@ pub(super) struct ChannelContext { pub interactive_tx_signing_session: Option, } -/// A channel struct implementing this trait can receive an initial counterparty commitment -/// transaction signature. -trait InitialRemoteCommitmentReceiver { - fn context(&self) -> &ChannelContext; - - fn context_mut(&mut self) -> &mut ChannelContext; - - fn funding(&self) -> &FundingScope; - - fn funding_mut(&mut self) -> &mut FundingScope; - - fn received_msg(&self) -> &'static str; - - #[rustfmt::skip] - fn check_counterparty_commitment_signature( - &self, sig: &Signature, holder_commitment_point: &HolderCommitmentPoint, logger: &L - ) -> Result { - let funding_script = self.funding().get_funding_redeemscript(); - - let commitment_data = self.context().build_commitment_transaction(self.funding(), - holder_commitment_point.next_transaction_number(), &holder_commitment_point.next_point(), - true, false, logger); - let initial_commitment_tx = commitment_data.tx; - let trusted_tx = initial_commitment_tx.trust(); - let initial_commitment_bitcoin_tx = trusted_tx.built_transaction(); - let sighash = initial_commitment_bitcoin_tx.get_sighash_all(&funding_script, self.funding().get_value_satoshis()); - // They sign the holder commitment transaction... - log_trace!(logger, "Checking {} tx signature {} by key {} against tx {} (sighash {}) with redeemscript {} for channel {}.", - self.received_msg(), log_bytes!(sig.serialize_compact()[..]), log_bytes!(self.funding().counterparty_funding_pubkey().serialize()), - encode::serialize_hex(&initial_commitment_bitcoin_tx.transaction), log_bytes!(sighash[..]), - encode::serialize_hex(&funding_script), &self.context().channel_id()); - secp_check!(self.context().secp_ctx.verify_ecdsa(&sighash, sig, self.funding().counterparty_funding_pubkey()), format!("Invalid {} signature from peer", self.received_msg())); - - Ok(initial_commitment_tx) - } - - #[rustfmt::skip] - fn initial_commitment_signed( - &mut self, channel_id: ChannelId, counterparty_signature: Signature, holder_commitment_point: &mut HolderCommitmentPoint, - best_block: BestBlock, signer_provider: &SP, logger: &L, - ) -> Result<(ChannelMonitor, CommitmentTransaction), ChannelError> { - let initial_commitment_tx = match self.check_counterparty_commitment_signature(&counterparty_signature, holder_commitment_point, logger) { - Ok(res) => res, - Err(ChannelError::Close(e)) => { - // TODO(dual_funding): Update for V2 established channels. - if !self.funding().is_outbound() { - self.funding_mut().channel_transaction_parameters.funding_outpoint = None; - } - return Err(ChannelError::Close(e)); - }, - Err(e) => { - // The only error we know how to handle is ChannelError::Close, so we fall over here - // to make sure we don't continue with an inconsistent state. - panic!("unexpected error type from check_counterparty_commitment_signature {:?}", e); - } - }; - let context = self.context(); - let commitment_data = context.build_commitment_transaction(self.funding(), - context.counterparty_next_commitment_transaction_number, - &context.counterparty_next_commitment_point.unwrap(), false, false, logger); - let counterparty_initial_commitment_tx = commitment_data.tx; - let counterparty_trusted_tx = counterparty_initial_commitment_tx.trust(); - let counterparty_initial_bitcoin_tx = counterparty_trusted_tx.built_transaction(); - - log_trace!(logger, "Initial counterparty tx for channel {} is: txid {} tx {}", - &context.channel_id(), counterparty_initial_bitcoin_tx.txid, encode::serialize_hex(&counterparty_initial_bitcoin_tx.transaction)); - - let holder_commitment_tx = HolderCommitmentTransaction::new( - initial_commitment_tx, - counterparty_signature, - Vec::new(), - &self.funding().get_holder_pubkeys().funding_pubkey, - &self.funding().counterparty_funding_pubkey() - ); - - if context.holder_signer.as_ref().validate_holder_commitment(&holder_commitment_tx, Vec::new()).is_err() { - return Err(ChannelError::close("Failed to validate our commitment".to_owned())); - } - - // Now that we're past error-generating stuff, update our local state: - - let is_v2_established = self.is_v2_established(); - let context = self.context_mut(); - context.channel_id = channel_id; - - assert!(!context.channel_state.is_monitor_update_in_progress()); // We have not had any monitor(s) yet to fail update! - if !is_v2_established { - if context.is_batch_funding() { - context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::WAITING_FOR_BATCH); - } else { - context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); - } - } - if holder_commitment_point.advance(&context.holder_signer, &context.secp_ctx, logger).is_err() { - // We only fail to advance our commitment point/number if we're currently - // waiting for our signer to unblock and provide a commitment point. - // We cannot send accept_channel/open_channel before this has occurred, so if we - // err here by the time we receive funding_created/funding_signed, something has gone wrong. - debug_assert!(false, "We should be ready to advance our commitment point by the time we receive {}", self.received_msg()); - return Err(ChannelError::close("Failed to advance holder commitment point".to_owned())); - } - - let context = self.context(); - let funding = self.funding(); - let obscure_factor = get_commitment_transaction_number_obscure_factor(&funding.get_holder_pubkeys().payment_point, &funding.get_counterparty_pubkeys().payment_point, funding.is_outbound()); - let shutdown_script = context.shutdown_scriptpubkey.clone().map(|script| script.into_inner()); - let monitor_signer = signer_provider.derive_channel_signer(context.channel_keys_id); - // TODO(RBF): When implementing RBF, the funding_txo passed here must only update - // ChannelMonitorImp::first_confirmed_funding_txo during channel establishment, not splicing - let channel_monitor = ChannelMonitor::new( - context.secp_ctx.clone(), monitor_signer, shutdown_script, - funding.get_holder_selected_contest_delay(), &context.destination_script, - &funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor, - holder_commitment_tx, best_block, context.counterparty_node_id, context.channel_id(), - context.is_manual_broadcast, - ); - channel_monitor.provide_initial_counterparty_commitment_tx( - counterparty_initial_commitment_tx.clone(), - ); - - self.context_mut().counterparty_next_commitment_transaction_number -= 1; - - Ok((channel_monitor, counterparty_initial_commitment_tx)) - } - - fn is_v2_established(&self) -> bool; -} - -impl InitialRemoteCommitmentReceiver for PendingV1Channel { - fn context(&self) -> &ChannelContext { - &self.context - } - - fn context_mut(&mut self) -> &mut ChannelContext { - &mut self.context - } - - fn funding(&self) -> &FundingScope { - &self.funding - } - - fn funding_mut(&mut self) -> &mut FundingScope { - &mut self.funding - } - - fn received_msg(&self) -> &'static str { - "funding_signed" - } - - fn is_v2_established(&self) -> bool { - false - } -} - -impl InitialRemoteCommitmentReceiver for InboundV1Channel { - fn context(&self) -> &ChannelContext { - &self.context - } - - fn context_mut(&mut self) -> &mut ChannelContext { - &mut self.context - } - - fn funding(&self) -> &FundingScope { - &self.funding - } - - fn funding_mut(&mut self) -> &mut FundingScope { - &mut self.funding - } - - fn received_msg(&self) -> &'static str { - "funding_created" - } - - fn is_v2_established(&self) -> bool { - false - } -} - -impl InitialRemoteCommitmentReceiver for FundedChannel { - fn context(&self) -> &ChannelContext { - &self.context - } - - fn context_mut(&mut self) -> &mut ChannelContext { - &mut self.context - } - - fn funding(&self) -> &FundingScope { - &self.funding - } - - fn funding_mut(&mut self) -> &mut FundingScope { - &mut self.funding - } - - fn received_msg(&self) -> &'static str { - "commitment_signed" - } - - fn is_v2_established(&self) -> bool { - let channel_parameters = &self.funding().channel_transaction_parameters; - // This will return false if `counterparty_parameters` is `None`, but for a `FundedChannel`, it - // should never be `None`. - debug_assert!(channel_parameters.counterparty_parameters.is_some()); - channel_parameters.counterparty_parameters.as_ref().is_some_and(|counterparty_parameters| { - self.context().channel_id().is_v2_channel_id( - &channel_parameters.holder_pubkeys.revocation_basepoint, - &counterparty_parameters.pubkeys.revocation_basepoint, - ) - }) - } -} - impl ChannelContext { #[rustfmt::skip] fn new_for_inbound_channel<'a, ES: EntropySource, F: FeeEstimator, L: Logger>( @@ -3669,7 +3678,7 @@ impl ChannelContext { msg_channel_reserve_satoshis: u64, msg_push_msat: u64, open_channel_fields: msgs::CommonOpenChannelFields, - ) -> Result<(FundingScope, ChannelContext), ChannelError> { + ) -> Result<(FundingScope, ChannelContext), ChannelError> { let logger = WithContext::from(logger, Some(counterparty_node_id), Some(open_channel_fields.temporary_channel_id), None); let announce_for_forwarding = if (open_channel_fields.channel_flags & 1) == 1 { true } else { false }; @@ -3828,7 +3837,7 @@ impl ChannelContext { #[cfg(any(test, fuzzing))] next_remote_fee: Mutex::new(PredictedNextFee::default()), - channel_transaction_parameters: ChannelTransactionParameters { + channel_transaction_parameters: PartialChannelTransactionParameters { holder_pubkeys: pubkeys, holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, is_outbound_from_holder: false, @@ -3846,6 +3855,7 @@ impl ChannelContext { funding_tx_confirmation_height: 0, short_channel_id: None, minimum_depth_override: None, + }; let channel_context = ChannelContext { user_id, @@ -4005,7 +4015,7 @@ impl ChannelContext { channel_keys_id: [u8; 32], holder_signer: SP::EcdsaSigner, _logger: L, - ) -> Result<(FundingScope, ChannelContext), APIError> { + ) -> Result<(FundingScope, ChannelContext), APIError> { // This will be updated with the counterparty contribution if this is a dual-funded channel let channel_value_satoshis = funding_satoshis; @@ -4077,7 +4087,7 @@ impl ChannelContext { #[cfg(any(test, fuzzing))] next_remote_fee: Mutex::new(PredictedNextFee::default()), - channel_transaction_parameters: ChannelTransactionParameters { + channel_transaction_parameters: PartialChannelTransactionParameters { holder_pubkeys: pubkeys, holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, is_outbound_from_holder: true, @@ -4093,6 +4103,7 @@ impl ChannelContext { funding_tx_confirmation_height: 0, short_channel_id: None, minimum_depth_override: None, + }; let channel_context = Self { user_id, @@ -4408,7 +4419,9 @@ impl ChannelContext { self.temporary_channel_id } - pub(super) fn minimum_depth(&self, funding: &FundingScope) -> Option { + pub(super) fn minimum_depth( + &self, funding: &FundingScope

, + ) -> Option { funding.minimum_depth_override.or(self.minimum_depth) } @@ -4446,7 +4459,7 @@ impl ChannelContext { /// `accept_channel2` message. #[rustfmt::skip] pub fn do_accept_channel_checks( - &mut self, funding: &mut FundingScope, default_limits: &ChannelHandshakeLimits, + &mut self, funding: &mut FundingScope, default_limits: &ChannelHandshakeLimits, their_features: &InitFeatures, common_fields: &msgs::CommonAcceptChannelFields, channel_reserve_satoshis: u64, ) -> Result<(), ChannelError> { @@ -4586,7 +4599,9 @@ impl ChannelContext { } /// Allowed in any state (including after shutdown), but will return none before TheirInitSent - pub fn get_holder_htlc_maximum_msat(&self, funding: &FundingScope) -> Option { + pub fn get_holder_htlc_maximum_msat( + &self, funding: &FundingScope

, + ) -> Option { funding.get_htlc_maximum_msat(self.holder_max_htlc_value_in_flight_msat) } @@ -4596,7 +4611,9 @@ impl ChannelContext { } /// Allowed in any state (including after shutdown), but will return none before TheirInitSent - pub fn get_counterparty_htlc_maximum_msat(&self, funding: &FundingScope) -> Option { + pub fn get_counterparty_htlc_maximum_msat( + &self, funding: &FundingScope

, + ) -> Option { funding.get_htlc_maximum_msat(self.counterparty_max_htlc_value_in_flight_msat) } @@ -4762,15 +4779,6 @@ impl ChannelContext { } } - #[rustfmt::skip] - fn unset_funding_info(&mut self, funding: &mut FundingScope) { - funding.channel_transaction_parameters.funding_outpoint = None; - self.channel_id = self.temporary_channel_id.expect( - "temporary_channel_id should be set since unset_funding_info is only called on funded \ - channels that were unfunded immediately beforehand" - ); - } - /// Returns a best-effort guess of the set of HTLCs that will be present /// on the next local or remote commitment. We cannot be certain as the /// actual set of HTLCs present on the next commitment depends on the @@ -4863,7 +4871,9 @@ impl ChannelContext { /// will *not* be present on the next commitment from `next_commitment_htlcs`, and /// check if their outcome is successful. If it is, we add the value of this claimed /// HTLC to the balance of the claimer. - fn get_next_commitment_value_to_self_msat(&self, local: bool, funding: &FundingScope) -> u64 { + fn get_next_commitment_value_to_self_msat( + &self, local: bool, funding: &FundingScope

, + ) -> u64 { use InboundHTLCRemovalReason::Fulfill; use OutboundHTLCOutcome::Success; @@ -4896,7 +4906,9 @@ impl ChannelContext { .saturating_add(inbound_claimed_htlc_msat) } - fn get_channel_constraints(&self, funding: &FundingScope) -> ChannelConstraints { + fn get_channel_constraints( + &self, funding: &FundingScope

, + ) -> ChannelConstraints { ChannelConstraints { holder_dust_limit_satoshis: self.holder_dust_limit_satoshis, counterparty_selected_channel_reserve_satoshis: funding @@ -4912,8 +4924,8 @@ impl ChannelContext { } } - fn get_next_local_commitment_stats( - &self, funding: &FundingScope, htlc_candidate: Option, + fn get_next_local_commitment_stats( + &self, funding: &FundingScope

, htlc_candidate: Option, include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize, feerate_per_kw: u32, dust_exposure_limiting_feerate: Option, ) -> Result<(ChannelStats, Vec), ()> { @@ -4979,8 +4991,8 @@ impl ChannelContext { Ok((local_stats, next_commitment_htlcs)) } - fn get_next_remote_commitment_stats( - &self, funding: &FundingScope, htlc_candidate: Option, + fn get_next_remote_commitment_stats( + &self, funding: &FundingScope

, htlc_candidate: Option, include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize, feerate_per_kw: u32, dust_exposure_limiting_feerate: Option, ) -> Result<(ChannelStats, Vec), ()> { @@ -5046,8 +5058,8 @@ impl ChannelContext { Ok((remote_stats, next_commitment_htlcs)) } - fn validate_update_add_htlc( - &self, funding: &FundingScope, msg: &msgs::UpdateAddHTLC, + fn validate_update_add_htlc( + &self, funding: &FundingScope

, msg: &msgs::UpdateAddHTLC, fee_estimator: &LowerBoundedFeeEstimator, ) -> Result<(), ChannelError> { if msg.amount_msat > funding.get_value_satoshis() * 1000 { @@ -5144,8 +5156,8 @@ impl ChannelContext { Ok(()) } - fn validate_update_fee( - &self, funding: &FundingScope, fee_estimator: &LowerBoundedFeeEstimator, + fn validate_update_fee( + &self, funding: &FundingScope

, fee_estimator: &LowerBoundedFeeEstimator, new_feerate_per_kw: u32, ) -> Result<(), ChannelError> { // Check that we won't be pushed over our dust exposure limit by the feerate increase. @@ -5213,8 +5225,9 @@ impl ChannelContext { } fn validate_commitment_signed( - &self, funding: &FundingScope, transaction_number: u64, commitment_point: PublicKey, - msg: &msgs::CommitmentSigned, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + &self, funding: &FundingScope, transaction_number: u64, + commitment_point: PublicKey, msg: &msgs::CommitmentSigned, + fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result< (HolderCommitmentTransaction, Vec<(HTLCOutputInCommitment, Option<&HTLCSource>)>), ChannelError, @@ -5340,8 +5353,8 @@ impl ChannelContext { Ok((holder_commitment_tx, commitment_data.htlcs_included)) } - fn can_send_update_fee( - &self, funding: &FundingScope, feerate_per_kw: u32, + fn can_send_update_fee( + &self, funding: &FundingScope

, feerate_per_kw: u32, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> bool { // Before proposing a feerate update, check that we can actually afford the new fee. @@ -5418,8 +5431,8 @@ impl ChannelContext { return true; } - fn can_accept_incoming_htlc( - &self, funding: &FundingScope, dust_exposure_limiting_feerate: Option, logger: &L, + fn can_accept_incoming_htlc( + &self, funding: &FundingScope

, dust_exposure_limiting_feerate: Option, logger: &L, ) -> Result<(), LocalHTLCFailureReason> { // The fee spike buffer (an additional nondust HTLC) we keep for the remote if the channel // is not zero fee. This deviates from the spec because the fee spike buffer requirement @@ -5536,7 +5549,7 @@ impl ChannelContext { #[inline] #[rustfmt::skip] - fn get_commitment_feerate(&self, funding: &FundingScope, generated_by_local: bool) -> u32 { + fn get_commitment_feerate(&self, funding: &FundingScope

, generated_by_local: bool) -> u32 { let mut feerate_per_kw = self.feerate_per_kw; if let Some((feerate, update_state)) = self.pending_update_fee { if match update_state { @@ -5568,7 +5581,7 @@ impl ChannelContext { /// which peer generated this transaction and "to whom" this transaction flows. #[inline] #[rustfmt::skip] - fn build_commitment_transaction(&self, funding: &FundingScope, commitment_number: u64, per_commitment_point: &PublicKey, local: bool, generated_by_local: bool, logger: &L) -> CommitmentData<'_> { + fn build_commitment_transaction(&self, funding: &FundingScope, commitment_number: u64, per_commitment_point: &PublicKey, local: bool, generated_by_local: bool, logger: &L) -> CommitmentData<'_> { let broadcaster_dust_limit_sat = if local { self.holder_dust_limit_satoshis } else { self.counterparty_dust_limit_satoshis }; let feerate_per_kw = self.get_commitment_feerate(funding, generated_by_local); @@ -5577,9 +5590,10 @@ impl ChannelContext { let mut value_to_self_claimed_msat = 0; let mut value_to_remote_claimed_msat = 0; + let channel_params = &funding.channel_transaction_parameters; log_trace!(logger, "Building commitment transaction number {} (really {} xor {}) for channel {} for {}, generated by {} with fee {}...", commitment_number, (INITIAL_COMMITMENT_NUMBER - commitment_number), - get_commitment_transaction_number_obscure_factor(&funding.get_holder_pubkeys().payment_point, &funding.get_counterparty_pubkeys().payment_point, funding.is_outbound()), + get_commitment_transaction_number_obscure_factor(&channel_params.holder_pubkeys.payment_point, &channel_params.counterparty_parameters.pubkeys.payment_point, channel_params.is_outbound_from_holder), self.channel_id, if local { "us" } else { "remote" }, if generated_by_local { "us" } else { "remote" }, feerate_per_kw); @@ -5752,7 +5766,7 @@ impl ChannelContext { /// Returns information on all pending inbound HTLCs. #[rustfmt::skip] - pub fn get_pending_inbound_htlc_details(&self, funding: &FundingScope) -> Vec { + pub fn get_pending_inbound_htlc_details(&self, funding: &FundingScope

) -> Vec { let mut holding_cell_states = new_hash_map(); for holding_cell_update in self.holding_cell_htlc_updates.iter() { match holding_cell_update { @@ -5802,7 +5816,7 @@ impl ChannelContext { /// Returns information on all pending outbound HTLCs. #[rustfmt::skip] - pub fn get_pending_outbound_htlc_details(&self, funding: &FundingScope) -> Vec { + pub fn get_pending_outbound_htlc_details(&self, funding: &FundingScope

) -> Vec { let mut outbound_details = Vec::new(); let dust_buffer_feerate = self.get_dust_buffer_feerate(None); @@ -5843,8 +5857,8 @@ impl ChannelContext { outbound_details } - fn get_available_balances_for_scope( - &self, funding: &FundingScope, fee_estimator: &LowerBoundedFeeEstimator, + fn get_available_balances_for_scope( + &self, funding: &FundingScope

, fee_estimator: &LowerBoundedFeeEstimator, ) -> Result { let htlc_candidate = None; let include_counterparty_unknown_htlcs = true; @@ -5913,15 +5927,19 @@ impl ChannelContext { /// /// Note that if [`Self::is_manual_broadcast`] is true the transaction will be a dummy /// transaction. - pub fn unbroadcasted_funding(&self, funding: &FundingScope) -> Option { + pub fn unbroadcasted_funding( + &self, funding: &FundingScope

, + ) -> Option { self.if_unbroadcasted_funding(|| funding.funding_transaction.clone()) } /// Returns the transaction ID if there is a pending funding transaction that is yet to be /// broadcast. - pub fn unbroadcasted_funding_txid(&self, funding: &FundingScope) -> Option { + pub fn unbroadcasted_funding_txid( + &self, funding: &FundingScope

, + ) -> Option { self.if_unbroadcasted_funding(|| { - funding.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) + funding.channel_transaction_parameters.funding_outpoint().map(|txo| txo.txid) }) } @@ -5932,14 +5950,16 @@ impl ChannelContext { /// Returns the transaction ID if there is a pending batch funding transaction that is yet to be /// broadcast. - pub fn unbroadcasted_batch_funding_txid(&self, funding: &FundingScope) -> Option { + pub fn unbroadcasted_batch_funding_txid( + &self, funding: &FundingScope

, + ) -> Option { self.unbroadcasted_funding_txid(funding).filter(|_| self.is_batch_funding()) } /// Shuts down this Channel (no more calls into this Channel may be made afterwards except /// those explicitly stated to be alowed after shutdown, e.g. some simple getters). - fn force_shutdown( - &mut self, funding: &FundingScope, mut closure_reason: ClosureReason, + fn force_shutdown( + &mut self, funding: &FundingScope

, mut closure_reason: ClosureReason, ) -> ShutdownResult { // Note that we MUST only generate a monitor update that indicates force-closure - we're // called during initialization prior to the chain_monitor in the encompassing ChannelManager @@ -6061,6 +6081,99 @@ impl ChannelContext { } } + #[rustfmt::skip] + fn initial_commitment_signed( + &mut self, funding: &FundingScope, + received_msg: &str, + channel_id: ChannelId, counterparty_signature: Signature, + holder_commitment_point: &mut HolderCommitmentPoint, + best_block: BestBlock, signer_provider: &SP, logger: &L, + ) -> Result<(ChannelMonitor, CommitmentTransaction), ChannelError> { + // Check counterparty commitment signature (inlined from former check_counterparty_commitment_signature) + let funding_script = funding.get_funding_redeemscript(); + + let commitment_data = self.build_commitment_transaction(funding, + holder_commitment_point.next_transaction_number(), &holder_commitment_point.next_point(), + true, false, logger); + let initial_commitment_tx = commitment_data.tx; + let trusted_tx = initial_commitment_tx.trust(); + let initial_commitment_bitcoin_tx = trusted_tx.built_transaction(); + let sighash = initial_commitment_bitcoin_tx.get_sighash_all(&funding_script, funding.get_value_satoshis()); + // They sign the holder commitment transaction... + log_trace!(logger, "Checking {} tx signature {} by key {} against tx {} (sighash {}) with redeemscript {} for channel {}.", + received_msg, log_bytes!(counterparty_signature.serialize_compact()[..]), log_bytes!(funding.counterparty_funding_pubkey().serialize()), + encode::serialize_hex(&initial_commitment_bitcoin_tx.transaction), log_bytes!(sighash[..]), + encode::serialize_hex(&funding_script), &self.channel_id()); + secp_check!(self.secp_ctx.verify_ecdsa(&sighash, &counterparty_signature, funding.counterparty_funding_pubkey()), format!("Invalid {} signature from peer", received_msg)); + + let commitment_data = self.build_commitment_transaction(funding, + self.counterparty_next_commitment_transaction_number, + &self.counterparty_next_commitment_point.unwrap(), false, false, logger); + let counterparty_initial_commitment_tx = commitment_data.tx; + let counterparty_trusted_tx = counterparty_initial_commitment_tx.trust(); + let counterparty_initial_bitcoin_tx = counterparty_trusted_tx.built_transaction(); + + log_trace!(logger, "Initial counterparty tx for channel {} is: txid {} tx {}", + &self.channel_id(), counterparty_initial_bitcoin_tx.txid, encode::serialize_hex(&counterparty_initial_bitcoin_tx.transaction)); + + let holder_commitment_tx = HolderCommitmentTransaction::new( + initial_commitment_tx, + counterparty_signature, + Vec::new(), + &funding.get_holder_pubkeys().funding_pubkey, + &funding.counterparty_funding_pubkey() + ); + + if self.holder_signer.as_ref().validate_holder_commitment(&holder_commitment_tx, Vec::new()).is_err() { + return Err(ChannelError::close("Failed to validate our commitment".to_owned())); + } + + // Now that we're past error-generating stuff, update our local state: + + let is_v2_established = channel_id.is_v2_channel_id( + &funding.channel_transaction_parameters.holder_pubkeys.revocation_basepoint, + &funding.channel_transaction_parameters.counterparty_parameters.pubkeys.revocation_basepoint, + ); + self.channel_id = channel_id; + + assert!(!self.channel_state.is_monitor_update_in_progress()); // We have not had any monitor(s) yet to fail update! + if !is_v2_established { + if self.is_batch_funding() { + self.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::WAITING_FOR_BATCH); + } else { + self.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); + } + } + if holder_commitment_point.advance(&self.holder_signer, &self.secp_ctx, logger).is_err() { + // We only fail to advance our commitment point/number if we're currently + // waiting for our signer to unblock and provide a commitment point. + // We cannot send accept_channel/open_channel before this has occurred, so if we + // err here by the time we receive funding_created/funding_signed, something has gone wrong. + debug_assert!(false, "We should be ready to advance our commitment point by the time we receive {}", received_msg); + return Err(ChannelError::close("Failed to advance holder commitment point".to_owned())); + } + + let obscure_factor = get_commitment_transaction_number_obscure_factor(&funding.get_holder_pubkeys().payment_point, &funding.get_counterparty_pubkeys().payment_point, funding.is_outbound()); + let shutdown_script = self.shutdown_scriptpubkey.clone().map(|script| script.into_inner()); + let monitor_signer = signer_provider.derive_channel_signer(self.channel_keys_id); + // TODO(RBF): When implementing RBF, the funding_txo passed here must only update + // ChannelMonitorImp::first_confirmed_funding_txo during channel establishment, not splicing + let channel_monitor = ChannelMonitor::new( + self.secp_ctx.clone(), monitor_signer, shutdown_script, + funding.get_holder_selected_contest_delay(), &self.destination_script, + &funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor, + holder_commitment_tx, best_block, self.counterparty_node_id, self.channel_id(), + self.is_manual_broadcast, + ); + channel_monitor.provide_initial_counterparty_commitment_tx( + counterparty_initial_commitment_tx.clone(), + ); + + self.counterparty_next_commitment_transaction_number -= 1; + + Ok((channel_monitor, counterparty_initial_commitment_tx)) + } + /// Only allowed after [`FundingScope::channel_transaction_parameters`] is set. #[rustfmt::skip] fn get_funding_signed_msg( @@ -6104,7 +6217,7 @@ impl ChannelContext { /// downgrade of channel features would be possible so that we can still open the channel. #[rustfmt::skip] pub(crate) fn maybe_downgrade_channel_features( - &mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator, + &mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator, user_config: &UserConfig, their_features: &InitFeatures, ) -> Result<(), ()> { if !funding.is_outbound() || @@ -6169,7 +6282,7 @@ impl ChannelContext { } fn get_initial_counterparty_commitment_signatures( - &self, funding: &FundingScope, logger: &L, + &self, funding: &FundingScope, logger: &L, ) -> Option<(Signature, Vec)> { let mut commitment_number = self.counterparty_next_commitment_transaction_number; let mut commitment_point = self.counterparty_next_commitment_point.unwrap(); @@ -6191,18 +6304,15 @@ impl ChannelContext { let counterparty_initial_commitment_tx = commitment_data.tx; match self.holder_signer { // TODO (taproot|arik): move match into calling method for Taproot - ChannelSignerType::Ecdsa(ref ecdsa) => { - let channel_parameters = &funding.channel_transaction_parameters; - ecdsa - .sign_counterparty_commitment( - channel_parameters, - &counterparty_initial_commitment_tx, - Vec::new(), - Vec::new(), - &self.secp_ctx, - ) - .ok() - }, + ChannelSignerType::Ecdsa(ref ecdsa) => ecdsa + .sign_counterparty_commitment( + &funding.channel_transaction_parameters, + &counterparty_initial_commitment_tx, + Vec::new(), + Vec::new(), + &self.secp_ctx, + ) + .ok(), // TODO (taproot|arik) #[cfg(taproot)] _ => todo!(), @@ -6210,7 +6320,7 @@ impl ChannelContext { } fn get_initial_commitment_signed_v2( - &mut self, funding: &FundingScope, logger: &L, + &mut self, funding: &FundingScope, logger: &L, ) -> Option { let signatures = self.get_initial_counterparty_commitment_signatures(funding, logger); if let Some((signature, htlc_signatures)) = signatures { @@ -6224,7 +6334,7 @@ impl ChannelContext { channel_id: self.channel_id, htlc_signatures, signature, - funding_txid: funding.get_funding_txo().map(|funding_txo| funding_txo.txid), + funding_txid: Some(funding.channel_transaction_parameters.funding_outpoint.txid), #[cfg(taproot)] partial_signature_with_nonce: None, }) @@ -6238,7 +6348,9 @@ impl ChannelContext { } } - fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { + fn check_funding_meets_minimum_depth( + &self, funding: &FundingScope

, height: u32, + ) -> bool { let minimum_depth = self .minimum_depth(funding) .expect("ChannelContext::minimum_depth should be set for FundedChannel"); @@ -6262,9 +6374,11 @@ impl ChannelContext { } #[rustfmt::skip] - fn check_for_funding_tx_confirmed( - &mut self, funding: &mut FundingScope, block_hash: &BlockHash, height: u32, - index_in_block: usize, tx: &mut ConfirmedTransaction, logger: &L, + fn check_for_funding_tx_confirmed( + &mut self, + funding: &mut FundingScope

, + block_hash: &BlockHash, height: u32, index_in_block: usize, + tx: &mut ConfirmedTransaction, logger: &L, ) -> Result { let funding_txo = match funding.get_funding_txo() { Some(funding_txo) => funding_txo, @@ -6428,13 +6542,17 @@ pub(super) struct FundingNegotiationContext { impl FundingNegotiationContext { /// Prepare and start interactive transaction negotiation. /// If error occurs, it is caused by our side, not the counterparty. - fn into_interactive_tx_constructor( - self, context: &ChannelContext, funding: &FundingScope, entropy_source: &ES, + fn into_interactive_tx_constructor< + P: ChannelTransactionParametersAccess, + SP: SignerProvider, + ES: EntropySource, + >( + self, context: &ChannelContext, funding: &FundingScope

, entropy_source: &ES, holder_node_id: PublicKey, ) -> (InteractiveTxConstructor, Option) { debug_assert_eq!( self.shared_funding_input.is_some(), - funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(), + funding.channel_transaction_parameters.splice_parent_funding_txid().is_some(), ); if self.shared_funding_input.is_some() { @@ -6494,7 +6612,7 @@ impl FundingNegotiationContext { // Counterparty designates channel data owned by the another channel participant entity. #[cfg_attr(test, derive(Debug))] pub(super) struct FundedChannel { - pub funding: FundingScope, + pub funding: FundingScope, pub context: ChannelContext, holder_commitment_point: HolderCommitmentPoint, @@ -6656,14 +6774,10 @@ macro_rules! maybe_create_splice_funding_failed { .and_then(|pending_splice| pending_splice.funding_negotiation.$get()) .filter(|funding_negotiation| funding_negotiation.is_initiator()) .map(|funding_negotiation| { - let funding_txo = funding_negotiation - .as_funding() - .and_then(|funding| funding.get_funding_txo()) - .map(|txo| txo.into_bitcoin_outpoint()); + let funding_txo = + funding_negotiation.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()); - let channel_type = funding_negotiation - .as_funding() - .map(|funding| funding.get_channel_type().clone()); + let channel_type = funding_negotiation.get_channel_type().cloned(); let (contributed_inputs, contributed_outputs) = match funding_negotiation { FundingNegotiation::AwaitingAck { context, .. } => { @@ -6706,6 +6820,14 @@ where &self.context } + fn is_v2_established(&self) -> bool { + let channel_parameters = &self.funding.channel_transaction_parameters; + self.context.channel_id().is_v2_channel_id( + &channel_parameters.holder_pubkeys.revocation_basepoint, + &channel_parameters.counterparty_parameters.pubkeys.revocation_basepoint, + ) + } + pub fn force_shutdown(&mut self, closure_reason: ClosureReason) -> ShutdownResult { let splice_funding_failed = self.maybe_fail_splice_negotiation(); @@ -6763,7 +6885,7 @@ where }) } - fn pending_funding(&self) -> &[FundingScope] { + fn pending_funding(&self) -> &[FundingScope] { if let Some(pending_splice) = &self.pending_splice { pending_splice.negotiated_candidates.as_slice() } else { @@ -6771,7 +6893,9 @@ where } } - fn funding_and_pending_funding_iter_mut(&mut self) -> impl Iterator { + fn funding_and_pending_funding_iter_mut( + &mut self, + ) -> impl Iterator> { core::iter::once(&mut self.funding).chain( self.pending_splice .as_mut() @@ -7010,7 +7134,7 @@ where } pub fn funding_outpoint(&self) -> OutPoint { - self.funding.channel_transaction_parameters.funding_outpoint.unwrap() + self.funding.channel_transaction_parameters.funding_outpoint } /// Claims an HTLC while we're disconnected from a peer, dropping the [`ChannelMonitorUpdate`] @@ -7355,31 +7479,6 @@ where self.context.channel_state.clear_waiting_for_batch(); } - /// Unsets the existing funding information for V1 funded channels. - /// - /// This must only be used if the channel has not yet completed funding and has not been used. - /// - /// Further, the channel must be immediately shut down after this with a call to - /// [`ChannelContext::force_shutdown`]. - pub fn unset_funding_info(&mut self) { - let sent_or_received_tx_signatures = self - .context - .interactive_tx_signing_session - .as_ref() - .map(|signing_session| { - signing_session.holder_tx_signatures().is_some() - || signing_session.has_received_tx_signatures() - }) - .unwrap_or(false); - debug_assert!( - matches!( - self.context.channel_state, - ChannelState::FundingNegotiated(_) if !sent_or_received_tx_signatures - ) || matches!(self.context.channel_state, ChannelState::AwaitingChannelReady(_)) - ); - self.context.unset_funding_info(&mut self.funding); - } - /// Handles a channel_ready message from our peer. If we've already sent our channel_ready /// and the channel is now usable (and public), this may generate an announcement_signatures to /// reply with. @@ -7777,7 +7876,9 @@ where "initial commitment_signed", ); - let (channel_monitor, _) = self.initial_commitment_signed( + let (channel_monitor, _) = self.context.initial_commitment_signed( + &self.funding, + "commitment_signed", self.context.channel_id(), msg.signature, holder_commitment_point, @@ -8011,8 +8112,7 @@ where let mut commitment_txs = Vec::with_capacity(self.pending_funding().len() + 1); let mut htlc_data = None; for funding in core::iter::once(&self.funding).chain(self.pending_funding().iter()) { - let funding_txid = - funding.get_funding_txid().expect("Funding txid must be known for pending scope"); + let funding_txid = funding.get_funding_txid(); let msg = messages.get(&funding_txid).ok_or_else(|| { ChannelError::close(format!( "Peer did not send a commitment_signed for pending splice transaction: {}", @@ -9807,7 +9907,7 @@ where // - MUST retransmit `announcement_signatures`. if let Some(funding_locked) = &msg.my_current_funding_locked { if funding_locked.should_retransmit(msgs::FundingLockedFlags::AnnouncementSignatures) { - if self.funding.get_funding_txid() == Some(funding_locked.txid) { + if self.funding.get_funding_txid() == funding_locked.txid { self.context.announcement_sigs_state = AnnouncementSigsState::NotSent; } } @@ -9889,7 +9989,7 @@ where } }) .or_else(|| Some(&self.funding)) - .filter(|funding| funding.get_funding_txid() == Some(funding_txid)) + .filter(|funding| funding.get_funding_txid() == funding_txid) .ok_or_else(|| { let message = "Failed to find funding for new commitment_signed".to_owned(); ChannelError::Close( @@ -10004,7 +10104,7 @@ where let inferred_splice_locked = msg.my_current_funding_locked.as_ref().and_then(|funding_locked| { self.pending_funding() .iter() - .find(|funding| funding.get_funding_txid() == Some(funding_locked.txid)) + .find(|funding| funding.get_funding_txid() == funding_locked.txid) .and_then(|_| { self.pending_splice.as_ref().and_then(|pending_splice| { (Some(funding_locked.txid) != pending_splice.received_funding_txid) @@ -11050,7 +11150,9 @@ where } } - fn check_funding_meets_minimum_depth(&self, funding: &FundingScope, height: u32) -> bool { + fn check_funding_meets_minimum_depth( + &self, funding: &FundingScope

, height: u32, + ) -> bool { self.context.check_funding_meets_minimum_depth(funding, height) } @@ -11093,7 +11195,7 @@ where let funding = pending_splice .negotiated_candidates .iter_mut() - .find(|funding| funding.get_funding_txid() == Some(splice_txid)) + .find(|funding| funding.get_funding_txid() == splice_txid) .unwrap(); let prev_funding_txid = self.funding.get_funding_txid(); @@ -11378,7 +11480,7 @@ where if funding.get_funding_tx_confirmations(height) == 0 { funding.funding_tx_confirmation_height = 0; if let Some(sent_funding_txid) = pending_splice.sent_funding_txid { - if Some(sent_funding_txid) == funding.get_funding_txid() { + if sent_funding_txid == funding.get_funding_txid() { log_warn!( logger, "Unconfirming sent splice_locked txid {} for channel {}", @@ -11427,24 +11529,11 @@ where } pub fn get_relevant_txids(&self) -> impl Iterator)> + '_ { - core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) - .map(|funding| { - ( - funding.get_funding_txid(), - funding.get_funding_tx_confirmation_height(), - funding.funding_tx_confirmed_in, - ) - }) - .filter_map(|(txid_opt, height_opt, hash_opt)| { - if let (Some(funding_txid), Some(conf_height), Some(block_hash)) = - (txid_opt, height_opt, hash_opt) - { - Some((funding_txid, conf_height, Some(block_hash))) - } else { - None - } - }) + core::iter::once(&self.funding).chain(self.pending_funding().iter()).filter_map(|funding| { + let conf_height = funding.get_funding_tx_confirmation_height()?; + let block_hash = funding.funding_tx_confirmed_in?; + Some((funding.get_funding_txid(), conf_height, Some(block_hash))) + }) } /// Checks if any funding transaction is no longer confirmed in the main chain. This may @@ -11457,7 +11546,7 @@ where ) -> Result<(), ClosureReason> { let unconfirmed_funding = self .funding_and_pending_funding_iter_mut() - .find(|funding| funding.get_funding_txid() == Some(*txid)); + .find(|funding| funding.get_funding_txid() == *txid); if let Some(funding) = unconfirmed_funding { if funding.funding_tx_confirmation_height != 0 { @@ -11714,9 +11803,7 @@ where self.pending_splice .as_ref() .and_then(|pending_splice| pending_splice.sent_funding_txid) - .or_else(|| { - self.is_our_channel_ready().then(|| self.funding.get_funding_txid()).flatten() - }) + .or_else(|| self.is_our_channel_ready().then(|| self.funding.get_funding_txid())) .map(|txid| { let mut funding_locked = msgs::FundingLocked { txid, retransmit_flags: 0 }; @@ -11727,7 +11814,7 @@ where // - otherwise: // - MUST set the `announcement_signatures` bit to `0` in `retransmit_flags`. if self.context.config.announce_for_forwarding { - if self.funding.get_funding_txid() != Some(txid) + if self.funding.get_funding_txid() != txid || self.context.announcement_sigs.is_none() { funding_locked.retransmit(msgs::FundingLockedFlags::AnnouncementSignatures); @@ -11961,12 +12048,8 @@ where debug_assert!(self.pending_splice.is_none()); // Rotate the funding pubkey using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); - let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { - (None, _) => { - debug_assert!(false); - self.funding.get_holder_pubkeys().funding_pubkey - }, - (Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => { + let funding_pubkey = match &self.context.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => { ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx) }, #[cfg(taproot)] @@ -12025,7 +12108,7 @@ where /// Checks during handling splice_init pub fn validate_splice_init( &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount, - ) -> Result { + ) -> Result, ChannelError> { if self.holder_commitment_point.current_point().is_none() { return Err(ChannelError::WarnAndDisconnect(format!( "Channel {} commitment point needs to be advanced once before spliced", @@ -12067,12 +12150,8 @@ where // Rotate the pubkeys using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); - let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { - (None, _) => { - debug_assert!(false); - self.funding.get_holder_pubkeys().funding_pubkey - }, - (Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => { + let funding_pubkey = match &self.context.holder_signer { + ChannelSignerType::Ecdsa(ecdsa) => { ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx) }, #[cfg(taproot)] @@ -12316,7 +12395,9 @@ where Ok(tx_msg_opt) } - fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result { + fn validate_splice_ack( + &self, msg: &msgs::SpliceAck, + ) -> Result, ChannelError> { // TODO(splicing): Add check that we are the splice (quiescence) initiator let pending_splice = self @@ -12361,8 +12442,8 @@ where )) } - fn get_holder_counterparty_balances_floor_incl_fee( - &self, funding: &FundingScope, + fn get_holder_counterparty_balances_floor_incl_fee( + &self, funding: &FundingScope

, ) -> Result<(Amount, Amount), String> { let include_counterparty_unknown_htlcs = true; // Make sure that that the funder of the channel can pay the transaction fees for an additional @@ -12436,7 +12517,7 @@ where if !pending_splice .negotiated_candidates .iter() - .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid)) + .any(|funding| funding.get_funding_txid() == msg.splice_txid) { let err = "unknown splice funding txid"; return Err(ChannelError::close(err.to_string())); @@ -12768,10 +12849,11 @@ where #[rustfmt::skip] fn build_commitment_no_state_update( - &self, funding: &FundingScope, logger: &L, + &self, funding: &FundingScope, logger: &L, ) -> (Vec<(HTLCOutputInCommitment, Option<&HTLCSource>)>, CommitmentTransaction) { let commitment_data = self.context.build_commitment_transaction( - funding, self.context.counterparty_next_commitment_transaction_number, + funding, + self.context.counterparty_next_commitment_transaction_number, &self.context.counterparty_next_commitment_point.unwrap(), false, true, logger, ); let counterparty_commitment_tx = commitment_data.tx; @@ -12792,14 +12874,15 @@ where #[rustfmt::skip] fn send_commitment_no_state_update_for_funding( - &self, funding: &FundingScope, logger: &L, + &self, funding: &FundingScope, logger: &L, ) -> Result { // Get the fee tests from `build_commitment_no_state_update` #[cfg(any(test, fuzzing))] self.build_commitment_no_state_update(funding, logger); let commitment_data = self.context.build_commitment_transaction( - funding, self.context.counterparty_next_commitment_transaction_number, + funding, + self.context.counterparty_next_commitment_transaction_number, &self.context.counterparty_next_commitment_point.unwrap(), false, true, logger, ); let counterparty_commitment_tx = commitment_data.tx; @@ -13332,7 +13415,7 @@ where /// A not-yet-funded outbound (from holder) channel using V1 channel establishment. pub(super) struct OutboundV1Channel { - pub funding: FundingScope, + pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, /// We tried to send an `open_channel` message but our commitment point wasn't ready. @@ -13437,8 +13520,12 @@ impl OutboundV1Channel { self.funding.funding_transaction = Some(funding_transaction); self.context.is_batch_funding = Some(()).filter(|_| is_batch_funding); + // Both late-bound fields are set: counterparty_parameters set during handshake, + // funding_outpoint set above. + let funding = self.funding.into_complete() + .expect("counterparty_parameters set during handshake, funding_outpoint set above"); let mut pending = PendingV1Channel { - funding: self.funding, + funding, context: self.context, unfunded_context: self.unfunded_context, }; @@ -13563,7 +13650,7 @@ impl OutboundV1Channel { /// An outbound channel using V1 channel establishment that has generated a `funding_created` /// message but has not yet received `funding_signed`. pub(super) struct PendingV1Channel { - pub funding: FundingScope, + pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, } @@ -13573,7 +13660,6 @@ impl PendingV1Channel { self.context.force_shutdown(&self.funding, closure_reason) } - /// Only allowed after [`FundingScope::channel_transaction_parameters`] is set. #[rustfmt::skip] fn get_funding_created_msg(&mut self, logger: &L) -> Option { let commitment_data = self.context.build_commitment_transaction(&self.funding, @@ -13583,8 +13669,7 @@ impl PendingV1Channel { let signature = match &self.context.holder_signer { // TODO (taproot|arik): move match into calling method for Taproot ChannelSignerType::Ecdsa(ecdsa) => { - let channel_parameters = &self.funding.channel_transaction_parameters; - ecdsa.sign_counterparty_commitment(channel_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.context.secp_ctx) + ecdsa.sign_counterparty_commitment(&self.funding.channel_transaction_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.context.secp_ctx) .map(|(sig, _)| sig).ok() }, // TODO (taproot|arik) @@ -13602,8 +13687,8 @@ impl PendingV1Channel { signature.map(|signature| msgs::FundingCreated { temporary_channel_id: self.context.temporary_channel_id.unwrap(), - funding_txid: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().txid, - funding_output_index: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().index, + funding_txid: self.funding.channel_transaction_parameters.funding_outpoint.txid, + funding_output_index: self.funding.channel_transaction_parameters.funding_outpoint.index, signature, #[cfg(taproot)] partial_signature_with_nonce: None, @@ -13612,6 +13697,23 @@ impl PendingV1Channel { }) } + /// Indicates that the signer may have some signatures for us, so we should retry if we're + /// blocked. + #[rustfmt::skip] + pub fn signer_maybe_unblocked( + &mut self, logger: &L, + ) -> Option { + if let Some(ref mut point) = self.unfunded_context.holder_commitment_point { + if !point.can_advance() { + point.try_resolve_pending(&self.context.holder_signer, &self.context.secp_ctx, logger); + } + } + if self.context.signer_pending_funding { + log_trace!(logger, "Attempting to generate pending funding created..."); + self.get_funding_created_msg(logger) + } else { None } + } + /// Handles a funding_signed message from the remote end. /// If this call is successful, broadcast the funding transaction (and not before!) pub fn funding_signed( @@ -13641,7 +13743,9 @@ impl PendingV1Channel { "funding_signed", ); - let (channel_monitor, _) = match self.initial_commitment_signed( + let (channel_monitor, _) = match self.context.initial_commitment_signed( + &self.funding, + "funding_signed", self.context.channel_id(), msg.signature, &mut holder_commitment_point, @@ -13649,8 +13753,10 @@ impl PendingV1Channel { signer_provider, logger, ) { - Ok(channel_monitor) => channel_monitor, - Err(err) => return Err((self, err)), + Ok(result) => result, + Err(err) => { + return Err((self, err)); + }, }; log_info!( @@ -13681,44 +13787,27 @@ impl PendingV1Channel { Ok((channel, channel_monitor)) } - /// Indicates that the signer may have some signatures for us, so we should retry if we're - /// blocked. - pub fn signer_maybe_unblocked( - &mut self, logger: &L, - ) -> Option { - if let Some(ref mut point) = self.unfunded_context.holder_commitment_point { - if !point.can_advance() { - point.try_resolve_pending( - &self.context.holder_signer, - &self.context.secp_ctx, - logger, - ); - } - } - if self.context.signer_pending_funding { - log_trace!(logger, "Attempting to generate pending funding created..."); - self.get_funding_created_msg(logger) - } else { - None - } - } - - /// Unsets the existing funding information. + /// Resets the channel ID to the temporary one so that error handling closes the + /// correct channel entry. /// /// The channel must be immediately shut down after this with a call to /// [`ChannelContext::force_shutdown`]. + #[rustfmt::skip] pub fn unset_funding_info(&mut self) { debug_assert!(matches!( self.context.channel_state, ChannelState::FundingNegotiated(_) if self.context.interactive_tx_signing_session.is_none() )); - self.context.unset_funding_info(&mut self.funding); + self.context.channel_id = self.context.temporary_channel_id.expect( + "temporary_channel_id should be set since unset_funding_info is only called on \ + channels that were unfunded immediately beforehand" + ); } } /// A not-yet-funded inbound (from counterparty) channel using V1 channel establishment. pub(super) struct InboundV1Channel { - pub funding: FundingScope, + pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, pub signer_pending_accept_channel: bool, @@ -13926,8 +14015,15 @@ impl InboundV1Channel { let funding_txo = OutPoint { txid: msg.funding_txid, index: msg.funding_output_index }; self.funding.channel_transaction_parameters.funding_outpoint = Some(funding_txo); - let (channel_monitor, counterparty_initial_commitment_tx) = match self - .initial_commitment_signed( + let funding = self + .funding + .into_complete() + .expect("channel_transaction_parameters must be complete for funding_created"); + + let (channel_monitor, counterparty_initial_commitment_tx) = + match self.context.initial_commitment_signed( + &funding, + "funding_created", ChannelId::v1_from_funding_outpoint(funding_txo), msg.signature, &mut holder_commitment_point, @@ -13935,12 +14031,22 @@ impl InboundV1Channel { signer_provider, logger, ) { - Ok(channel_monitor) => channel_monitor, - Err(err) => return Err((self, err)), - }; + Ok(result) => result, + Err(err) => { + return Err(( + InboundV1Channel { + funding: funding.into_partial(), + context: self.context, + unfunded_context: self.unfunded_context, + signer_pending_accept_channel: self.signer_pending_accept_channel, + }, + err, + )); + }, + }; let funding_signed = self.context.get_funding_signed_msg( - &self.funding.channel_transaction_parameters, + &funding.channel_transaction_parameters, logger, counterparty_initial_commitment_tx, ); @@ -13955,7 +14061,7 @@ impl InboundV1Channel { // Promote the channel to a full-fledged one now that we have updated the state and have a // `ChannelMonitor`. let mut channel = FundedChannel { - funding: self.funding, + funding, context: self.context, holder_commitment_point, pending_splice: None, @@ -13997,9 +14103,17 @@ impl InboundV1Channel { } } +/// A V2 channel that has completed funding transaction construction but has not yet +/// received `commitment_signed`. +pub(super) struct PendingV2Channel { + pub funding: FundingScope, + pub context: ChannelContext, + pub unfunded_context: UnfundedChannelContext, +} + // A not-yet-funded channel using V2 channel establishment. pub(super) struct UnfundedV2Channel { - pub funding: FundingScope, + pub funding: FundingScope, pub context: ChannelContext, pub unfunded_context: UnfundedChannelContext, pub funding_negotiation_context: FundingNegotiationContext, @@ -14321,20 +14435,6 @@ impl UnfundedV2Channel { } } -/// A V2 channel that has completed funding transaction construction but has not yet -/// received `commitment_signed`. -pub(super) struct PendingV2Channel { - pub funding: FundingScope, - pub context: ChannelContext, - pub unfunded_context: UnfundedChannelContext, -} - -impl PendingV2Channel { - pub fn abandon_unfunded_chan(&mut self, closure_reason: ClosureReason) -> ShutdownResult { - self.context.force_shutdown(&self.funding, closure_reason) - } -} - // Unfunded channel utilities pub(super) fn get_initial_channel_type( @@ -16062,7 +16162,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (_, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16205,7 +16305,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (mut node_b_chan, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16401,7 +16501,7 @@ mod tests { value: Amount::from_sat(10000000), script_pubkey: output_script.clone(), }]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); + let (node_a_chan, funding_created_msg) = node_a_chan.get_funding_created(tx.clone(), funding_outpoint, false, &&logger).map_err(|_| ()).unwrap(); let (_, funding_signed_msg, _) = node_b_chan.funding_created(&funding_created_msg.unwrap(), best_block, &&keys_provider, &&logger).map_err(|_| ()).unwrap(); // Node B --> Node A: funding signed @@ -16510,7 +16610,7 @@ mod tests { }], }; let funding_outpoint = OutPoint { txid: tx.compute_txid(), index: 0 }; - let (_pending, funding_created) = outbound_chan + let (_pending_chan, funding_created) = outbound_chan .get_funding_created(tx.clone(), funding_outpoint, false, &&logger) .map_err(|_| ()) .unwrap(); @@ -18170,7 +18270,7 @@ mod tests { }, ]}; let funding_outpoint = OutPoint{ txid: tx.compute_txid(), index: 0 }; - let (mut node_a_chan, funding_created_msg) = node_a_chan.get_funding_created( + let (node_a_chan, funding_created_msg) = node_a_chan.get_funding_created( tx.clone(), funding_outpoint, true, &&logger, ).map_err(|_| ()).unwrap(); let (mut node_b_chan, funding_signed_msg, _) = node_b_chan.funding_created( diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index 5547bee8f4c..901ed94bb52 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -15,7 +15,8 @@ use bitcoin::secp256k1::PublicKey; use crate::chain::chaininterface::{FeeEstimator, LowerBoundedFeeEstimator}; use crate::chain::transaction::OutPoint; -use crate::ln::channel::Channel; +use crate::ln::chan_utils::ChannelTransactionParametersAccess; +use crate::ln::channel::{AvailableBalances, Channel, ChannelContext, FundingScope}; use crate::ln::types::ChannelId; use crate::sign::SignerProvider; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; @@ -523,18 +524,16 @@ impl ChannelDetails { channel: &Channel, best_block_height: u32, latest_features: InitFeatures, fee_estimator: &LowerBoundedFeeEstimator, ) -> Self { - let context = channel.context(); - let funding = channel.funding(); - let balance_result = channel.get_available_balances(fee_estimator); - let balance = balance_result.unwrap_or_else(|()| { - debug_assert!(false, "some channel balance has been overdrawn"); - crate::ln::channel::AvailableBalances { - inbound_capacity_msat: 0, - outbound_capacity_msat: 0, - next_outbound_htlc_limit_msat: 0, - next_outbound_htlc_minimum_msat: u64::MAX, - } - }); + channel.channel_details(best_block_height, latest_features, fee_estimator) + } + + #[rustfmt::skip] + pub(super) fn from_channel_parts( + context: &ChannelContext, funding: &FundingScope

, + balance: AvailableBalances, minimum_depth: Option, + best_block_height: u32, latest_features: InitFeatures, + _fee_estimator: &LowerBoundedFeeEstimator, + ) -> Self { let (to_remote_reserve_satoshis, to_self_reserve_satoshis) = funding.get_holder_counterparty_selected_channel_reserve_satoshis(); #[allow(deprecated)] // TODO: Remove once balance_msat is removed. @@ -583,7 +582,7 @@ impl ChannelDetails { next_outbound_htlc_limit_msat: balance.next_outbound_htlc_limit_msat, next_outbound_htlc_minimum_msat: balance.next_outbound_htlc_minimum_msat, user_channel_id: context.get_user_id(), - confirmations_required: channel.minimum_depth(), + confirmations_required: minimum_depth, confirmations: Some(funding.get_funding_tx_confirmations(best_block_height)), force_close_spend_delay: funding.get_counterparty_selected_contest_delay(), is_outbound: funding.is_outbound(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c6764917b8d..63163dd0c6e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -54,7 +54,9 @@ use crate::events::{ InboundChannelFunds, PaymentFailureReason, ReplayEvent, }; use crate::events::{FundingInfo, PaidBolt12Invoice}; -use crate::ln::chan_utils::selected_commitment_sat_per_1000_weight; +use crate::ln::chan_utils::{ + selected_commitment_sat_per_1000_weight, ChannelTransactionParametersAccess, +}; #[cfg(any(test, fuzzing, feature = "_test_utils"))] use crate::ln::channel::QuiescentAction; use crate::ln::channel::QuiescentError; @@ -1717,9 +1719,8 @@ impl PeerState { return false; } } - let chan_is_funded_or_outbound = |(_, channel): (_, &Channel)| { - channel.is_funded() || channel.funding().is_outbound() - }; + let chan_is_funded_or_outbound = + |(_, channel): (_, &Channel)| channel.is_funded() || channel.is_outbound(); !self.channel_by_id.iter().any(chan_is_funded_or_outbound) && self.monitor_update_blocked_actions.is_empty() && self.closed_channel_monitor_update_ids.is_empty() @@ -10898,7 +10899,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if accept_0conf { // This should have been correctly configured by the call to Inbound(V1/V2)Channel::new. debug_assert!(channel.minimum_depth().unwrap() == 0); - } else if channel.funding().get_channel_type().requires_zero_conf() { + } else if channel.get_channel_type().requires_zero_conf() { let send_msg_err_event = MessageSendEvent::HandleError { node_id: channel.context().get_counterparty_node_id(), action: msgs::ErrorAction::SendErrorMessage { @@ -10998,7 +10999,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, None => { // Outbound channels don't contribute to the unfunded count in the DoS context. - if chan.funding().is_outbound() { + if chan.is_outbound() { continue; } @@ -11173,7 +11174,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - let (mut chan, funding_msg_opt, monitor) = match peer_state + let (chan, funding_msg_opt, monitor) = match peer_state .channel_by_id .remove(&msg.temporary_channel_id) .map(Channel::into_unfunded_inbound_v1) @@ -11229,8 +11230,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // immediately, we'll remove the existing channel from `outpoint_to_peer`. // Thus, we must first unset the funding outpoint on the channel. let err = ChannelError::close($err.to_owned()); - chan.unset_funding_info(); let mut chan = Channel::from(chan); + chan.unfund(); return Err(self.locked_handle_unfunded_close(err, &mut chan).1); }}; } @@ -11407,36 +11408,36 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { let chan = chan_entry.get_mut(); - match chan - .funding_signed(&msg, best_block, &self.signer_provider, &self.logger) - .and_then(|(funded_chan, monitor)| { - self.chain_monitor - .watch_channel(funded_chan.context.channel_id(), monitor) - .map_err(|()| { + match chan.funding_signed(&msg, best_block, &self.signer_provider, &self.logger) { + Ok((funded_chan, monitor)) => { + let channel_id = funded_chan.context.channel_id(); + match self.chain_monitor.watch_channel(channel_id, monitor) { + Ok(persist_status) => { + if let Some(data) = self.handle_initial_monitor( + &mut peer_state.in_flight_monitor_updates, + &mut peer_state.monitor_update_blocked_actions, + &mut peer_state.pending_msg_events, + peer_state.is_connected, + funded_chan, + persist_status, + ) { + mem::drop(peer_state_lock); + mem::drop(per_peer_state); + self.handle_post_monitor_update_chan_resume(data); + } + Ok(()) + }, + Err(()) => { // We weren't able to watch the channel to begin with, so no // updates should be made on it. Previously, full_stack_target // found an (unreachable) panic when the monitor update contained // within `shutdown_finish` was applied. - funded_chan.unset_funding_info(); - ChannelError::close("Channel ID was a duplicate".to_owned()) - }) - .map(|persist_status| (funded_chan, persist_status)) - }) - { - Ok((funded_chan, persist_status)) => { - if let Some(data) = self.handle_initial_monitor( - &mut peer_state.in_flight_monitor_updates, - &mut peer_state.monitor_update_blocked_actions, - &mut peer_state.pending_msg_events, - peer_state.is_connected, - funded_chan, - persist_status, - ) { - mem::drop(peer_state_lock); - mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + chan.unfund(); + try_channel_entry!(self, peer_state, Err(ChannelError::close( + "Channel ID was a duplicate".to_owned() + )), chan_entry) + }, } - Ok(()) }, Err(e) => try_channel_entry!(self, peer_state, Err(e), chan_entry), } @@ -12309,7 +12310,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ hash_map::Entry::Occupied(mut chan_entry) => { let chan = chan_entry.get_mut(); let logger = WithChannelContext::from(&self.logger, &chan.context(), None); - let funding_txo = chan.funding().get_funding_txo(); + let funding_txo = chan.get_funding_txo(); let res = chan.commitment_signed( msg, best_block, @@ -12383,7 +12384,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ hash_map::Entry::Occupied(mut chan_entry) => { let chan = chan_entry.get_mut(); let logger = WithChannelContext::from(&self.logger, &chan.context(), None); - let funding_txo = chan.funding().get_funding_txo(); + let funding_txo = chan.get_funding_txo(); if let Some(chan) = chan.as_funded_mut() { let monitor_update_opt = try_channel_entry!( self, peer_state, chan.commitment_signed_batch(batch, &self.fee_estimator, &&logger), chan_entry diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 2d971c3a100..b29cf2860f7 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1212,7 +1212,7 @@ macro_rules! get_channel_type_features { peer_state_lock, $channel_id ); - chan.funding().get_channel_type().clone() + chan.get_channel_type().clone() }}; } diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index d88b9a2dc3f..01551b0f9c1 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -85,7 +85,13 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { temp_channel_id ); assert!(channel.is_unfunded_v1()); - channel.funding_mut().holder_selected_channel_reserve_satoshis = 0; + if let Some(chan) = channel.as_unfunded_outbound_v1_mut() { + chan.funding.holder_selected_channel_reserve_satoshis = 0; + } else if let Some(chan) = channel.as_unfunded_inbound_v1_mut() { + chan.funding.holder_selected_channel_reserve_satoshis = 0; + } else { + panic!("expected unfunded v1 channel"); + } channel.context_mut().holder_max_htlc_value_in_flight_msat = 100_000_000; } @@ -905,11 +911,13 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { let channel = get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, chan.2); let chan_signer = channel.as_funded().unwrap().get_signer(); + let complete_params = + channel.as_funded().unwrap().funding.channel_transaction_parameters.clone(); let (commitment_tx, _stats) = SpecTxBuilder {}.build_commitment_transaction( false, commitment_number, &remote_point, - &channel.funding().channel_transaction_parameters, + &complete_params, &secp_ctx, local_chan_balance_msat, vec![accepted_htlc_info], @@ -917,11 +925,16 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { MIN_CHAN_DUST_LIMIT_SATOSHIS, &nodes[0].logger, ); - let params = &channel.funding().channel_transaction_parameters; chan_signer .as_ecdsa() .unwrap() - .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) + .sign_counterparty_commitment( + &complete_params, + &commitment_tx, + Vec::new(), + Vec::new(), + &secp_ctx, + ) .unwrap() }; @@ -2199,7 +2212,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { let chan = get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, chan_id); assert_eq!( - chan.funding().holder_selected_channel_reserve_satoshis, + chan.as_funded().unwrap().funding.holder_selected_channel_reserve_satoshis, channel_reserve_satoshis ); } @@ -2359,6 +2372,8 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, chan_id); let chan_signer = channel.as_funded().unwrap().get_signer(); + let complete_params = + channel.as_funded().unwrap().funding.channel_transaction_parameters.clone(); let commitment_tx = CommitmentTransaction::new( commitment_number, &remote_point, @@ -2366,15 +2381,14 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { local_chan_balance, FEERATE_PER_KW, htlcs, - &channel.funding().channel_transaction_parameters.as_counterparty_broadcastable(), + &complete_params.as_counterparty_broadcastable(), &secp_ctx, ); - let params = &channel.funding().channel_transaction_parameters; chan_signer .as_ecdsa() .unwrap() .sign_counterparty_commitment( - params, + &complete_params, &commitment_tx, Vec::new(), Vec::new(), diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index b5cbe0fee98..974a433137b 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2035,7 +2035,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.funding().get_short_channel_id().unwrap(), + channel_1.get_short_channel_id().unwrap(), ); assert_eq!(chan_1_used_liquidity, None); } @@ -2048,7 +2048,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.funding().get_short_channel_id().unwrap(), + channel_2.get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, None); @@ -2076,7 +2076,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.funding().get_short_channel_id().unwrap(), + channel_1.get_short_channel_id().unwrap(), ); // First hop accounts for expected 1000 msat fee assert_eq!(chan_1_used_liquidity, Some(501000)); @@ -2090,7 +2090,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.funding().get_short_channel_id().unwrap(), + channel_2.get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, Some(500000)); @@ -2118,7 +2118,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.funding().get_short_channel_id().unwrap(), + channel_1.get_short_channel_id().unwrap(), ); assert_eq!(chan_1_used_liquidity, None); } @@ -2131,7 +2131,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_2_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_b_id), &NodeId::from_pubkey(&node_c_id), - channel_2.funding().get_short_channel_id().unwrap(), + channel_2.get_short_channel_id().unwrap(), ); assert_eq!(chan_2_used_liquidity, None); } @@ -2181,7 +2181,7 @@ fn test_holding_cell_inflight_htlcs() { let used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel.funding().get_short_channel_id().unwrap(), + channel.get_short_channel_id().unwrap(), ); assert_eq!(used_liquidity, Some(2000000)); diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs index ac566393bdb..b7bf9cc03a4 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -483,6 +483,8 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann let local_chan_signer = local_chan.as_funded().unwrap().get_signer(); let nondust_htlcs: Vec = vec![]; + let complete_params = + local_chan.as_funded().unwrap().funding.channel_transaction_parameters.clone(); let commitment_tx = CommitmentTransaction::new( INITIAL_COMMITMENT_NUMBER - 1, &remote_point, @@ -492,14 +494,19 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann - commit_tx_fee_msat(non_buffer_feerate + 4, 0, &channel_type_features) / 1000, non_buffer_feerate + 4, nondust_htlcs, - &local_chan.funding().channel_transaction_parameters.as_counterparty_broadcastable(), + &complete_params.as_counterparty_broadcastable(), &secp_ctx, ); - let params = &local_chan.funding().channel_transaction_parameters; local_chan_signer .as_ecdsa() .unwrap() - .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) + .sign_counterparty_commitment( + &complete_params, + &commitment_tx, + Vec::new(), + Vec::new(), + &secp_ctx, + ) .unwrap() }; @@ -583,6 +590,8 @@ pub fn test_update_fee_that_saturates_subs() { get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, chan_id); let local_chan_signer = local_chan.as_funded().unwrap().get_signer(); let nondust_htlcs: Vec = vec![]; + let complete_params = + local_chan.as_funded().unwrap().funding.channel_transaction_parameters.clone(); let commitment_tx = CommitmentTransaction::new( INITIAL_COMMITMENT_NUMBER, &remote_point, @@ -592,14 +601,19 @@ pub fn test_update_fee_that_saturates_subs() { 0, FEERATE, nondust_htlcs, - &local_chan.funding().channel_transaction_parameters.as_counterparty_broadcastable(), + &complete_params.as_counterparty_broadcastable(), &secp_ctx, ); - let params = &local_chan.funding().channel_transaction_parameters; local_chan_signer .as_ecdsa() .unwrap() - .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) + .sign_counterparty_commitment( + &complete_params, + &commitment_tx, + Vec::new(), + Vec::new(), + &secp_ctx, + ) .unwrap() }; @@ -1083,7 +1097,7 @@ pub fn do_cannot_afford_on_holding_cell_release( let chan = get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, chan_id); assert_eq!( - chan.funding().holder_selected_channel_reserve_satoshis, + chan.as_funded().unwrap().funding.holder_selected_channel_reserve_satoshis, channel_reserve_satoshis ); } @@ -1270,7 +1284,7 @@ pub fn do_can_afford_given_trimmed_htlcs(inequality_regions: core::cmp::Ordering let chan = get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, chan_id); assert_eq!( - chan.funding().holder_selected_channel_reserve_satoshis, + chan.as_funded().unwrap().funding.holder_selected_channel_reserve_satoshis, channel_reserve_satoshis ); } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 84bfbb902ea..b59dc27b017 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -1617,24 +1617,18 @@ impl ChannelSigner for InMemorySigner { } } -const MISSING_PARAMS_ERR: &'static str = - "ChannelTransactionParameters must be populated before signing operations"; - impl EcdsaChannelSigner for InMemorySigner { fn sign_counterparty_commitment( &self, channel_parameters: &ChannelTransactionParameters, commitment_tx: &CommitmentTransaction, _inbound_htlc_preimages: Vec, _outbound_htlc_preimages: Vec, secp_ctx: &Secp256k1, ) -> Result<(Signature, Vec), ()> { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let trusted_tx = commitment_tx.trust(); let keys = trusted_tx.keys(); let funding_key = self.funding_key(channel_parameters.splice_parent_funding_txid); let funding_pubkey = funding_key.public_key(secp_ctx); - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let channel_funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &counterparty_keys.funding_pubkey); @@ -1693,12 +1687,9 @@ impl EcdsaChannelSigner for InMemorySigner { &self, channel_parameters: &ChannelTransactionParameters, commitment_tx: &HolderCommitmentTransaction, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_key = self.funding_key(channel_parameters.splice_parent_funding_txid); let funding_pubkey = funding_key.public_key(secp_ctx); - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &counterparty_keys.funding_pubkey); let trusted_tx = commitment_tx.trust(); @@ -1716,12 +1707,9 @@ impl EcdsaChannelSigner for InMemorySigner { &self, channel_parameters: &ChannelTransactionParameters, commitment_tx: &HolderCommitmentTransaction, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_key = self.funding_key(channel_parameters.splice_parent_funding_txid); let funding_pubkey = funding_key.public_key(secp_ctx); - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &counterparty_keys.funding_pubkey); let trusted_tx = commitment_tx.trust(); @@ -1739,8 +1727,6 @@ impl EcdsaChannelSigner for InMemorySigner { input: usize, amount: u64, per_commitment_key: &SecretKey, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let revocation_key = chan_utils::derive_private_revocation_key( &secp_ctx, &per_commitment_key, @@ -1753,8 +1739,7 @@ impl EcdsaChannelSigner for InMemorySigner { &per_commitment_point, ); let witness_script = { - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let holder_selected_contest_delay = channel_parameters.holder_selected_contest_delay; let counterparty_delayedpubkey = DelayedPaymentKey::from_basepoint( &secp_ctx, @@ -1786,8 +1771,6 @@ impl EcdsaChannelSigner for InMemorySigner { input: usize, amount: u64, per_commitment_key: &SecretKey, htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let revocation_key = chan_utils::derive_private_revocation_key( &secp_ctx, &per_commitment_key, @@ -1800,8 +1783,7 @@ impl EcdsaChannelSigner for InMemorySigner { &per_commitment_point, ); let witness_script = { - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let counterparty_htlcpubkey = HtlcKey::from_basepoint( &secp_ctx, &counterparty_keys.htlc_basepoint, @@ -1838,10 +1820,6 @@ impl EcdsaChannelSigner for InMemorySigner { &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, secp_ctx: &Secp256k1, ) -> Result { - let channel_parameters = - &htlc_descriptor.channel_derivation_parameters.transaction_parameters; - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let witness_script = htlc_descriptor.witness_script(secp_ctx); let sighash = &sighash::SighashCache::new(&*htlc_tx) .p2wsh_signature_hash( @@ -1865,8 +1843,6 @@ impl EcdsaChannelSigner for InMemorySigner { input: usize, amount: u64, per_commitment_point: &PublicKey, htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let htlc_key = chan_utils::derive_private_key(&secp_ctx, &per_commitment_point, &self.htlc_base_key); let revocation_pubkey = RevocationKey::from_basepoint( @@ -1874,8 +1850,7 @@ impl EcdsaChannelSigner for InMemorySigner { &channel_parameters.holder_pubkeys.revocation_basepoint, &per_commitment_point, ); - let counterparty_keys = - channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR); + let counterparty_keys = &channel_parameters.counterparty_parameters.pubkeys; let counterparty_htlcpubkey = HtlcKey::from_basepoint( &secp_ctx, &counterparty_keys.htlc_basepoint, @@ -1909,12 +1884,10 @@ impl EcdsaChannelSigner for InMemorySigner { &self, channel_parameters: &ChannelTransactionParameters, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1, ) -> Result { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); - let funding_key = self.funding_key(channel_parameters.splice_parent_funding_txid); let funding_pubkey = funding_key.public_key(secp_ctx); let counterparty_funding_key = - &channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR).funding_pubkey; + &channel_parameters.counterparty_parameters.pubkeys.funding_pubkey; let channel_funding_redeemscript = make_funding_redeemscript(&funding_pubkey, counterparty_funding_key); Ok(closing_tx.trust().sign( @@ -1929,8 +1902,6 @@ impl EcdsaChannelSigner for InMemorySigner { &self, chan_params: &ChannelTransactionParameters, anchor_tx: &Transaction, input: usize, secp_ctx: &Secp256k1, ) -> Result { - assert!(chan_params.is_populated(), "Channel parameters must be fully populated"); - let witness_script = chan_utils::get_keyed_anchor_redeemscript(&chan_params.holder_pubkeys.funding_pubkey); let amt = Amount::from_sat(ANCHOR_OUTPUT_VALUE_SATOSHI); @@ -1954,20 +1925,15 @@ impl EcdsaChannelSigner for InMemorySigner { &self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1, ) -> Signature { - assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); assert_eq!( tx.input[input_index].previous_output, - channel_parameters - .funding_outpoint - .as_ref() - .expect("Funding outpoint must be known prior to signing") - .into_bitcoin_outpoint() + channel_parameters.funding_outpoint.into_bitcoin_outpoint() ); let funding_key = self.funding_key(channel_parameters.splice_parent_funding_txid); let funding_pubkey = funding_key.public_key(secp_ctx); let counterparty_funding_key = - &channel_parameters.counterparty_pubkeys().expect(MISSING_PARAMS_ERR).funding_pubkey; + &channel_parameters.counterparty_parameters.pubkeys.funding_pubkey; let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, counterparty_funding_key); let sighash = &sighash::SighashCache::new(tx) diff --git a/lightning/src/util/test_channel_signer.rs b/lightning/src/util/test_channel_signer.rs index 70eb3223bc4..8b1a3b9bcb4 100644 --- a/lightning/src/util/test_channel_signer.rs +++ b/lightning/src/util/test_channel_signer.rs @@ -429,7 +429,7 @@ impl EcdsaChannelSigner for TestChannelSigner { .unwrap(); let countersignatory_htlc_key = HtlcKey::from_basepoint( &secp_ctx, - &channel_parameters.counterparty_pubkeys().unwrap().htlc_basepoint, + &channel_parameters.counterparty_parameters.pubkeys.htlc_basepoint, &htlc_descriptor.per_commitment_point, ); @@ -482,7 +482,7 @@ impl EcdsaChannelSigner for TestChannelSigner { return Err(()); } closing_tx - .verify(channel_parameters.funding_outpoint.as_ref().unwrap().into_bitcoin_outpoint()) + .verify(channel_parameters.funding_outpoint.into_bitcoin_outpoint()) .expect("derived different closing transaction"); Ok(self.inner.sign_closing_transaction(channel_parameters, closing_tx, secp_ctx).unwrap()) }