Skip to content

Validate negative funding contributions in splice_init and splice_ack messages #4011

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 98 additions & 46 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10748,7 +10748,9 @@ where
// Note on channel reserve requirement pre-check: as the splice acceptor does not contribute,
// it can't go below reserve, therefore no pre-check is done here.

// TODO(splicing): Early check for reserve requirement
if their_funding_contribution_satoshis.is_negative() {
self.validate_their_negative_funding_contribution(&splice_funding)?;
}

Ok(splice_funding)
}
Expand Down Expand Up @@ -10821,7 +10823,7 @@ where
})
}

/// Handle splice_ack
/// See also [`validate_splice_ack`]
#[cfg(splicing)]
pub(crate) fn splice_ack<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES,
Expand All @@ -10831,26 +10833,66 @@ where
ES::Target: EntropySource,
L::Target: Logger,
{
let pending_splice = if let Some(ref mut pending_splice) = &mut self.pending_splice {
pending_splice
let splice_funding = self.validate_splice_ack(msg)?;

log_info!(
logger,
"Starting splice funding negotiation for channel {} after receiving splice_ack; new channel value: {} sats (old: {} sats)",
self.context.channel_id,
splice_funding.get_value_satoshis(),
self.funding.get_value_satoshis(),
);

let pending_splice =
self.pending_splice.as_mut().expect("We should have returned an error earlier!");
// TODO: Good candidate for a let else statement once MSRV >= 1.65
let funding_negotiation_context = if let Some(FundingNegotiation::AwaitingAck(context)) =
pending_splice.funding_negotiation.take()
{
context
} else {
return Err(ChannelError::Ignore(format!("Channel is not in pending splice")));
panic!("We should have returned an error earlier!");
};

let mut interactive_tx_constructor = funding_negotiation_context
.into_interactive_tx_constructor(
&self.context,
&splice_funding,
signer_provider,
entropy_source,
holder_node_id.clone(),
)
.map_err(|err| {
ChannelError::WarnAndDisconnect(format!(
"Failed to start interactive transaction construction, {:?}",
err
))
})?;
let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message();

debug_assert!(self.interactive_tx_signing_session.is_none());
pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction(
splice_funding,
interactive_tx_constructor,
));

Ok(tx_msg_opt)
}

/// Checks during handling splice_ack
#[cfg(splicing)]
fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result<FundingScope, ChannelError> {
// TODO(splicing): Add check that we are the splice (quiescence) initiator

let funding_negotiation_context = match pending_splice.funding_negotiation.take() {
let funding_negotiation_context = match &self
.pending_splice
.as_ref()
.ok_or(ChannelError::Ignore(format!("Channel is not in pending splice")))?
.funding_negotiation
{
Some(FundingNegotiation::AwaitingAck(context)) => context,
Some(FundingNegotiation::ConstructingTransaction(funding, constructor)) => {
pending_splice.funding_negotiation =
Some(FundingNegotiation::ConstructingTransaction(funding, constructor));
return Err(ChannelError::WarnAndDisconnect(format!(
"Got unexpected splice_ack; splice negotiation already in progress"
)));
},
Some(FundingNegotiation::AwaitingSignatures(funding)) => {
pending_splice.funding_negotiation =
Some(FundingNegotiation::AwaitingSignatures(funding));
Some(FundingNegotiation::ConstructingTransaction(_, _))
| Some(FundingNegotiation::AwaitingSignatures(_)) => {
return Err(ChannelError::WarnAndDisconnect(format!(
"Got unexpected splice_ack; splice negotiation already in progress"
)));
Expand All @@ -10874,40 +10916,50 @@ where
msg.funding_pubkey,
)?;

// TODO(splicing): Pre-check for reserve requirement
// (Note: It should also be checked later at tx_complete)
if their_funding_contribution_satoshis.is_negative() {
self.validate_their_negative_funding_contribution(&splice_funding)?;
}

log_info!(
logger,
"Starting splice funding negotiation for channel {} after receiving splice_ack; new channel value: {} sats (old: {} sats)",
self.context.channel_id,
splice_funding.get_value_satoshis(),
self.funding.get_value_satoshis(),
);
Ok(splice_funding)
}

let mut interactive_tx_constructor = funding_negotiation_context
.into_interactive_tx_constructor(
&self.context,
&splice_funding,
signer_provider,
entropy_source,
holder_node_id.clone(),
)
.map_err(|err| {
ChannelError::WarnAndDisconnect(format!(
"Failed to start interactive transaction construction, {:?}",
err
))
})?;
let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message();
/// Used to validate a negative `funding_contribution_satoshis` in `splice_init` and `splice_ack` messages.
#[cfg(splicing)]
fn validate_their_negative_funding_contribution(
&self, spliced_funding: &FundingScope,
) -> Result<(), ChannelError> {
// Calculate the remote's new balance
//
// We only validate the remote's balance on the next *remote* commitment transaction.
//
// We don't care for a small / no to_remote output on our next *local* commitment transaction as the purpose of the
// channel reserve is to ensure we can punish *them* if they misbehave. See `validate_update_add_htlc` for another
// place where we apply the same reasoning.
//
// This comment is only relevant if they are the funder of the channel because the remote balance will be the same
// on both local and remote commitments if they are the fundee.
let spliced_commitment_stats =
self.context.build_commitment_stats(&spliced_funding, false, true, None, None);
let spliced_remote_balance_msat = if spliced_funding.is_outbound() {
spliced_commitment_stats.remote_balance_before_fee_msat
} else {
spliced_commitment_stats
.remote_balance_before_fee_msat
.saturating_sub(spliced_commitment_stats.commit_tx_fee_sat * 1000)
};

debug_assert!(self.interactive_tx_signing_session.is_none());
pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction(
splice_funding,
interactive_tx_constructor,
));
// Check if the remote's new balance is under the specified reserve
if spliced_remote_balance_msat
< spliced_funding.holder_selected_channel_reserve_satoshis * 1000
{
return Err(ChannelError::Warn(format!(
"Remote balance below reserve mandated by holder: {} vs {}",
spliced_remote_balance_msat,
spliced_funding.holder_selected_channel_reserve_satoshis * 1000,
)));
}

Ok(tx_msg_opt)
Ok(())
}

#[cfg(splicing)]
Expand Down
Loading