Skip to content

[Splicing] Tx negotiation during splicing #3736

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

Merged
merged 9 commits into from
Jul 31, 2025
71 changes: 45 additions & 26 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,16 @@ impl ChannelError {
pub(super) fn close(err: String) -> Self {
ChannelError::Close((err.clone(), ClosureReason::ProcessingError { err }))
}

pub(super) fn message(&self) -> &str {
match self {
&ChannelError::Ignore(ref e) => &e,
&ChannelError::Warn(ref e) => &e,
&ChannelError::WarnAndDisconnect(ref e) => &e,
&ChannelError::Close((ref e, _)) => &e,
&ChannelError::SendError(ref e) => &e,
}
}
}

pub(super) struct WithChannelContext<'a, L: Deref>
Expand Down Expand Up @@ -1774,7 +1784,7 @@ where

pub fn funding_tx_constructed<L: Deref>(
&mut self, logger: &L,
) -> Result<(msgs::CommitmentSigned, Option<Event>), ChannelError>
) -> Result<(msgs::CommitmentSigned, Option<Event>), msgs::TxAbort>
where
L::Target: Logger,
{
Expand Down Expand Up @@ -1829,14 +1839,17 @@ where
}
}

return Err(ChannelError::Warn(
"Got a tx_complete message in an invalid state".to_owned(),
));
return Err(msgs::TxAbort {
channel_id: chan.context.channel_id(),
data: "Got a tx_complete message in an invalid state".to_owned().into_bytes(),
});
},
_ => {
return Err(ChannelError::Warn(
"Got a tx_complete message in an invalid phase".to_owned(),
))
debug_assert!(false);
return Err(msgs::TxAbort {
channel_id: self.context().channel_id(),
data: "Got a tx_complete message in an invalid phase".to_owned().into_bytes(),
});
},
}
}
Expand Down Expand Up @@ -5476,7 +5489,7 @@ where
fn funding_tx_constructed<L: Deref>(
&mut self, funding: &mut FundingScope, signing_session: &mut InteractiveTxSigningSession,
is_splice: bool, holder_commitment_transaction_number: u64, logger: &L
) -> Result<(msgs::CommitmentSigned, Option<Event>), ChannelError>
) -> Result<(msgs::CommitmentSigned, Option<Event>), msgs::TxAbort>
where
L::Target: Logger
{
Expand All @@ -5485,19 +5498,21 @@ where
for (idx, outp) in signing_session.unsigned_tx().outputs().enumerate() {
if outp.script_pubkey() == &expected_spk && outp.value() == funding.get_value_satoshis() {
if output_index.is_some() {
let msg = "Multiple outputs matched the expected script and value";
let reason = ClosureReason::ProcessingError { err: msg.to_owned() };
return Err(ChannelError::Close((msg.to_owned(), reason)));
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: "Multiple outputs matched the expected script and value".to_owned().into_bytes(),
});
}
output_index = Some(idx as u16);
}
}
let outpoint = if let Some(output_index) = output_index {
OutPoint { txid: signing_session.unsigned_tx().compute_txid(), index: output_index }
} else {
let msg = "No output matched the funding script_pubkey";
let reason = ClosureReason::ProcessingError { err: msg.to_owned() };
return Err(ChannelError::Close((msg.to_owned(), reason)));
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: "No output matched the funding script_pubkey".to_owned().into_bytes(),
});
};
funding
.channel_transaction_parameters.funding_outpoint = Some(outpoint);
Expand All @@ -5507,12 +5522,11 @@ where
holder_commitment_transaction_number,
self.cur_counterparty_commitment_transaction_number,
);
let message = "TODO Forced error, incomplete implementation".to_owned();
// TODO(splicing) Forced error, as the use case is not complete
return Err(ChannelError::Close((
message.clone(),
ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message }
)));
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: "Splicing not yet supported".to_owned().into_bytes(),
});
} else {
self.assert_no_commitment_advancement(holder_commitment_transaction_number, "initial commitment_signed");
}
Expand All @@ -5522,7 +5536,10 @@ where
Ok(commitment_signed) => commitment_signed,
Err(e) => {
funding.channel_transaction_parameters.funding_outpoint = None;
return Err(e)
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: e.message().to_owned().into_bytes(),
});
},
};

Expand All @@ -5532,9 +5549,10 @@ where
false,
"Zero inputs were provided & zero witnesses were provided, but a count mismatch was somehow found",
);
let msg = "V2 channel rejected due to sender error";
let reason = ClosureReason::ProcessingError { err: msg.to_owned() };
return Err(ChannelError::Close((msg.to_owned(), reason)));
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: "V2 channel rejected due to sender error".to_owned().into_bytes(),
});
}
None
} else {
Expand All @@ -5556,9 +5574,10 @@ where
false,
"We don't support users providing inputs but somehow we had more than zero inputs",
);
let msg = "V2 channel rejected due to sender error";
let reason = ClosureReason::ProcessingError { err: msg.to_owned() };
return Err(ChannelError::Close((msg.to_owned(), reason)));
return Err(msgs::TxAbort {
channel_id: self.channel_id(),
data: "V2 channel rejected due to sender error".to_owned().into_bytes(),
});
};

let mut channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new());
Expand Down
21 changes: 19 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9730,10 +9730,27 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
peer_state.pending_msg_events.push(msg_send_event);
};
if negotiation_complete {
let (commitment_signed, funding_ready_for_sig_event_opt) = chan_entry
let (commitment_signed, funding_ready_for_sig_event_opt) = match chan_entry
.get_mut()
.funding_tx_constructed(&self.logger)
.map_err(|err| MsgHandleErrInternal::send_err_msg_no_close(format!("{}", err), msg.channel_id))?;
{
Ok((commitment_signed, event)) => (commitment_signed, event),
Err(tx_abort) => {
if chan_entry.get().is_funded() {
peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it applies here since the negotiated completed and we consumed the constructor, but generally when we send this we want to make sure we're no longer tracking one, and also probably assert that tx_signatures have not been exchanged.

We'll also want to follow-up with sending tx_abort in most other error cases until the tx_signatures exchange.

node_id: counterparty_node_id,
msg: tx_abort,
});
return Ok(());
} else {
let msg = String::from_utf8(tx_abort.data)
.expect("tx_abort data should contain valid UTF-8");
let reason = ClosureReason::ProcessingError { err: msg.clone() };
let err = ChannelError::Close((msg, reason));
try_channel_entry!(self, peer_state, Err(err), chan_entry)
}
},
};
if let Some(funding_ready_for_sig_event) = funding_ready_for_sig_event_opt {
let mut pending_events = self.pending_events.lock().unwrap();
pending_events.push_back((funding_ready_for_sig_event, None));
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/splicing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ fn test_v1_splice_in() {
_ => panic!("Unexpected event {:?}", events[0]),
}
match events[1] {
MessageSendEvent::HandleError { .. } => {},
MessageSendEvent::SendTxAbort { .. } => {},
_ => panic!("Unexpected event {:?}", events[1]),
}

Expand Down