Support async signing of splice shared input
What changed, and why it matters
This commit adds support for asynchronous signing of the shared input in a Lightning channel splice. Previously, the signature for the 2-of-2 multisig input had to be produced immediately when requested, which could block users whose signing hardware or policy requires delays. The change allows the signer to return an error and retry later, and it reworks the internal state machine so the splice negotiation waits cleanly until that signature is available. It is a feature/robustness improvement rather than a fix for an active exploit.
Review as a normal robustness/feature commit. Validate that the new async path cannot leave a channel stuck indefinitely, that re-entrancy in signer_unblocked is safe, and that the partial-signature state cannot be confused with a fully signed state. No urgent security response is indicated by the diff alone.
Security signals we found
API change to allow signer to refuse producing a signature and retry later
State-machine change to avoid sending incomplete tx_signatures while waiting for shared-input signature
New test covering async splice shared-input signature unblock path
Rework of FundingTxSigned bundling to coordinate commitment_signed, tx_signatures, splice_locked, and funding_tx broadcast on signer resume
No direct memory-safety, cryptographic, or remote-exploitable vulnerability visible in the diff
Evidence from the diff
The patch changes EcdsaChannelSigner::sign_splice_shared_input to return Result
Changed components
lightning/src/sign/ecdsa.rslightning/src/sign/mod.rslightning/src/util/dyn_signer.rslightning/src/util/test_channel_signer.rslightning/src/ln/interactivetxs.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/async_signer_tests.rsInspect captured patch +350 / −97
diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs
index ae73dd8..8edff20 100644
--- a/lightning/src/ln/async_signer_tests.rs
+++ b/lightning/src/ln/async_signer_tests.rs
@@ -1742,3 +1742,80 @@ fn test_async_splice_initial_commit_sig_waits_for_monitor_before_tx_signatures()
let _ = get_event!(initiator, Event::SpliceNegotiated);
let _ = get_event!(acceptor, Event::SpliceNegotiated);
}
+
+#[test]
+fn test_async_splice_shared_input_signature_released_on_unblock() {
+ // Test that we can provide the signature of a splice's shared input asynchronously, and check
+ // that the holding cell is freed after exiting quiescence due to exchanging `tx_signatures`.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2;
+
+ let (initiator, acceptor) = (&nodes[0], &nodes[1]);
+ let initiator_node_id = initiator.node.get_our_node_id();
+ let acceptor_node_id = acceptor.node.get_our_node_id();
+
+ initiator.disable_channel_signer_op(
+ &acceptor_node_id,
+ &channel_id,
+ SignerOp::SignSpliceSharedInput,
+ );
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(1_000),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }];
+ let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap();
+ negotiate_splice_tx(initiator, acceptor, channel_id, contribution);
+
+ let event = get_event!(initiator, Event::FundingTransactionReadyForSigning);
+ if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event {
+ let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap();
+ initiator
+ .node
+ .funding_transaction_signed(&channel_id, &acceptor_node_id, partially_signed_tx)
+ .unwrap();
+ }
+
+ let initiator_commit_sig = get_htlc_update_msgs(initiator, &acceptor_node_id);
+ acceptor
+ .node
+ .handle_commitment_signed(initiator_node_id, &initiator_commit_sig.commitment_signed[0]);
+ check_added_monitors(acceptor, 1);
+
+ let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events();
+ assert_eq!(acceptor_msg_events.len(), 2, "{acceptor_msg_events:?}");
+ for msg_event in &acceptor_msg_events {
+ match msg_event {
+ MessageSendEvent::UpdateHTLCs { updates, .. } => {
+ initiator
+ .node
+ .handle_commitment_signed(acceptor_node_id, &updates.commitment_signed[0]);
+ check_added_monitors(initiator, 1);
+ },
+ MessageSendEvent::SendTxSignatures { msg, .. } => {
+ initiator.node.handle_tx_signatures(acceptor_node_id, msg);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ }
+
+ assert!(initiator.node.get_and_clear_pending_msg_events().is_empty());
+
+ initiator.enable_channel_signer_op(
+ &acceptor_node_id,
+ &channel_id,
+ SignerOp::SignSpliceSharedInput,
+ );
+ initiator.node.signer_unblocked(None);
+
+ let tx_signatures =
+ get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id);
+ acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures);
+
+ let _ = get_event!(initiator, Event::SpliceNegotiated);
+ let _ = get_event!(acceptor, Event::SpliceNegotiated);
+}
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 8075699..3d6342c 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -1249,8 +1249,7 @@ pub(super) struct SignerResumeUpdates {
pub accept_channel: Option<msgs::AcceptChannel>,
pub funding_created: Option<msgs::FundingCreated>,
pub funding_signed: Option<msgs::FundingSigned>,
- pub funding_commit_sig: Option<msgs::CommitmentSigned>,
- pub tx_signatures: Option<msgs::TxSignatures>,
+ pub funding_tx_signed: Option<FundingTxSigned>,
pub channel_ready: Option<msgs::ChannelReady>,
pub order: RAACommitmentOrder,
pub closing_signed: Option<msgs::ClosingSigned>,
@@ -1683,11 +1682,11 @@ where
#[rustfmt::skip]
pub fn signer_maybe_unblocked<L: Logger, CBP>(
- &mut self, chain_hash: ChainHash, logger: &L, path_for_release_htlc: CBP
+ &mut self, chain_hash: ChainHash, best_block_height: u32, logger: &L, path_for_release_htlc: CBP
) -> Result<Option<SignerResumeUpdates>, ChannelError> where CBP: Fn(u64) -> BlindedMessagePath {
match &mut self.phase {
ChannelPhase::Undefined => unreachable!(),
- ChannelPhase::Funded(chan) => chan.signer_maybe_unblocked(logger, path_for_release_htlc).map(|r| Some(r)),
+ ChannelPhase::Funded(chan) => chan.signer_maybe_unblocked(best_block_height, logger, path_for_release_htlc).map(|r| Some(r)),
ChannelPhase::UnfundedOutboundV1(chan) => {
let (open_channel, funding_created) = chan.signer_maybe_unblocked(chain_hash, logger);
Ok(Some(SignerResumeUpdates {
@@ -1697,8 +1696,7 @@ where
accept_channel: None,
funding_created,
funding_signed: None,
- funding_commit_sig: None,
- tx_signatures: None,
+ funding_tx_signed: None,
channel_ready: None,
order: chan.context.resend_order.clone(),
closing_signed: None,
@@ -1715,8 +1713,7 @@ where
accept_channel,
funding_created: None,
funding_signed: None,
- funding_commit_sig: None,
- tx_signatures: None,
+ funding_tx_signed: None,
channel_ready: None,
order: chan.context.resend_order.clone(),
closing_signed: None,
@@ -2217,9 +2214,7 @@ where
.unwrap_or(false));
}
- if signing_session.has_holder_tx_signatures() {
- // Our `tx_signatures` either should've been the first time we processed them,
- // or we're waiting for our counterparty to send theirs first.
+ if signing_session.has_holder_witnesses() {
return Ok(FundingTxSigned {
commitment_signed: None,
counterparty_initial_commitment_signed_result: None,
@@ -2248,36 +2243,42 @@ where
return Err(APIError::APIMisuseError { err });
};
- let tx = signing_session.unsigned_tx().tx();
- if funding_txid_signed != tx.compute_txid() {
- return Err(APIError::APIMisuseError {
- err: "Transaction was malleated prior to signing".to_owned(),
- });
- }
+ let (mut tx_signatures, mut funding_tx) = signing_session
+ .provide_holder_witnesses(
+ context.channel_id,
+ funding_txid_signed,
+ witnesses,
+ &context.secp_ctx,
+ )
+ .map_err(|err| APIError::APIMisuseError { err })?;
- let shared_input_signature =
- if let Some(splice_input_index) = signing_session.unsigned_tx().shared_input_index() {
- let sig = context.holder_signer.sign_splice_shared_input(
+ debug_assert_eq!(
+ pending_splice.is_some(),
+ signing_session.unsigned_tx().shared_input_index().is_some()
+ );
+ if let Some(splice_input_index) = signing_session.unsigned_tx().shared_input_index() {
+ let sig = context
+ .holder_signer
+ .sign_splice_shared_input(
&funding.channel_transaction_parameters,
- tx,
+ signing_session.unsigned_tx().tx(),
splice_input_index as usize,
&context.secp_ctx,
- );
- Some(sig)
+ )
+ .ok();
+ if let Some(sig) = sig {
+ (tx_signatures, funding_tx) = signing_session
+ .provide_holder_shared_input_signature(sig)
+ .map_err(|err| APIError::APIMisuseError { err })?;
} else {
- None
- };
- debug_assert_eq!(pending_splice.is_some(), shared_input_signature.is_some());
-
- let tx_signatures = msgs::TxSignatures {
- channel_id: context.channel_id,
- tx_hash: funding_txid_signed,
- witnesses,
- shared_input_signature,
- };
- let (tx_signatures, funding_tx) = signing_session
- .provide_holder_witnesses(tx_signatures, &context.secp_ctx)
- .map_err(|err| APIError::APIMisuseError { err })?;
+ log_debug!(
+ logger,
+ "Splice shared input signature not available, waiting on async signer"
+ );
+ debug_assert!(tx_signatures.is_none());
+ debug_assert!(funding_tx.is_none());
+ }
+ }
let logger = WithChannelContext::from(logger, &context, None);
if tx_signatures.is_some() {
@@ -2409,18 +2410,17 @@ where
// which must always come after the initial commitment signed is sent.
.unwrap_or(true);
let res = if has_negotiated_pending_splice && !session_received_commitment_signed {
- let has_holder_tx_signatures = funded_channel
+ let has_holder_witnesses = funded_channel
.context
.interactive_tx_signing_session
.as_ref()
- .map(|session| session.has_holder_tx_signatures())
+ .map(|session| session.has_holder_witnesses())
.unwrap_or(false);
// We delay processing this until the user manually approves the splice via
- // [`Channel::funding_transaction_signed`], as otherwise, there would be a
- // [`ChannelMonitorUpdateStep::RenegotiatedFunding`] committed that we would
- // need to undo if they no longer wish to proceed.
- if has_holder_tx_signatures {
+ // [`Channel::funding_transaction_signed`], as otherwise, it would prevent the
+ // user from canceling their contribution if they no longer wish to proceed.
+ if has_holder_witnesses {
funded_channel
.splice_initial_commitment_signed(msg, fee_estimator, logger)
.map(|monitor_update_opt| (None, monitor_update_opt))
@@ -5179,7 +5179,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
ChannelState::FundingNegotiated(_) => self
.interactive_tx_signing_session
.as_ref()
- .map(|signing_session| signing_session.has_holder_tx_signatures())
+ .map(|signing_session| signing_session.has_holder_witnesses())
.unwrap_or(false),
ChannelState::AwaitingChannelReady(flags) => !flags.is_waiting_for_batch(),
_ => true,
@@ -7910,7 +7910,7 @@ where
.interactive_tx_signing_session
.as_ref()
.map(|signing_session| {
- signing_session.has_holder_tx_signatures()
+ signing_session.has_holder_witnesses()
|| signing_session.has_received_tx_signatures()
})
.unwrap_or(false);
@@ -9584,6 +9584,8 @@ where
}
}
+ let awaiting_holder_shared_input_signature =
+ signing_session.awaiting_holder_shared_input_signature();
let (holder_tx_signatures, funding_tx) =
signing_session.received_tx_signatures(msg).map_err(|msg| ChannelError::Warn(msg))?;
@@ -9622,6 +9624,11 @@ where
best_block_height,
&logger,
);
+ } else if awaiting_holder_shared_input_signature {
+ log_debug!(
+ logger,
+ "Waiting for funding transaction shared input signature before finalizing negotiation"
+ );
} else {
debug_assert!(
false,
@@ -10016,7 +10023,7 @@ where
/// blocked.
#[rustfmt::skip]
pub fn signer_maybe_unblocked<L: Logger, CBP>(
- &mut self, logger: &L, path_for_release_htlc: CBP
+ &mut self, best_block_height: u32, logger: &L, path_for_release_htlc: CBP
) -> Result<SignerResumeUpdates, ChannelError> where CBP: Fn(u64) -> BlindedMessagePath {
if let Some((commitment_number, commitment_secret)) = self.context.signer_pending_stale_state_verification.clone() {
if let Ok(expected_point) = self
@@ -10072,16 +10079,65 @@ where
None
};
- let tx_signatures = if funding_commit_sig.is_some() {
+ let mut shared_input_signature_unblocked = false;
+ {
+ if let Some(signing_session) = self.context.interactive_tx_signing_session.as_mut() {
+ if signing_session.awaiting_holder_shared_input_signature() {
+ let splice_input_index = signing_session
+ .unsigned_tx()
+ .shared_input_index()
+ .expect("Missing shared input index while awaiting a splice signature");
+ log_trace!(logger, "Attempting to generate pending splice shared input signature...");
+ if let Ok(shared_input_signature) = self.context.holder_signer.sign_splice_shared_input(
+ &self.funding.channel_transaction_parameters,
+ signing_session.unsigned_tx().tx(),
+ splice_input_index as usize,
+ &self.context.secp_ctx,
+ ) {
+ shared_input_signature_unblocked = true;
+ signing_session
+ .provide_holder_shared_input_signature(shared_input_signature)
+ .map_err(ChannelError::close)?;
+ }
+ }
+ }
+ }
+
+ let mut tx_signatures = None;
+ let mut funding_tx = None;
+ if funding_commit_sig.is_some() || shared_input_signature_unblocked {
if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() {
- signing_session.holder_tx_signatures().filter(|_| !self.is_awaiting_monitor_update())
+ if !self.is_awaiting_monitor_update() && !self.context.signer_pending_funding {
+ tx_signatures = signing_session.holder_tx_signatures();
+ funding_tx = tx_signatures.as_ref().and_then(|_| signing_session.signed_tx());
+ }
} else {
debug_assert!(false);
- None
}
- } else {
- None
- };
+ }
+
+ let mut funding_tx_signed = None;
+ if funding_commit_sig.is_some() || tx_signatures.is_some() || funding_tx.is_some() {
+ let mut resumed = FundingTxSigned {
+ commitment_signed: funding_commit_sig,
+ counterparty_initial_commitment_signed_result: None,
+ tx_signatures,
+ funding_tx: None,
+ splice_negotiated: None,
+ splice_locked: None,
+ };
+ if let Some(funding_tx) = funding_tx {
+ let funding_logger = WithChannelContext::from(logger, &self.context, None);
+ debug_assert!(resumed.tx_signatures.is_some());
+ self.on_tx_signatures_exchange(
+ &mut resumed,
+ funding_tx,
+ best_block_height,
+ &funding_logger,
+ );
+ }
+ funding_tx_signed = Some(resumed);
+ }
// Provide a `channel_ready` message if we need to, but only if we're _not_ still pending
// funding.
@@ -10147,8 +10203,8 @@ where
if revoke_and_ack.is_some() { "a" } else { "no" },
self.context.resend_order,
if funding_signed.is_some() { "a" } else { "no" },
- if funding_commit_sig.is_some() { "a" } else { "no" },
- if tx_signatures.is_some() { "a" } else { "no" },
+ if funding_tx_signed.as_ref().map(|v| v.commitment_signed.is_some()).unwrap_or(false) { "a" } else { "no" },
+ if funding_tx_signed.as_ref().map(|v| v.tx_signatures.is_some()).unwrap_or(false) { "a" } else { "no" },
if channel_ready.is_some() { "a" } else { "no" },
if closing_signed.is_some() { "a" } else { "no" },
if signed_closing_tx.is_some() { "a" } else { "no" },
@@ -10161,8 +10217,7 @@ where
accept_channel: None,
funding_created: None,
funding_signed,
- funding_commit_sig,
- tx_signatures,
+ funding_tx_signed,
channel_ready,
order: self.context.resend_order.clone(),
closing_signed,
@@ -10512,7 +10567,7 @@ where
} else {
tx_signatures = Some(holder_tx_signatures);
}
- } else if !session.has_holder_tx_signatures() {
+ } else if !session.has_holder_witnesses() {
log_debug!(logger, "Waiting for funding transaction signatures to be provided");
}
} else {
@@ -10948,7 +11003,7 @@ where
matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_));
if matches!(self.context.channel_state, ChannelState::FundingNegotiated(_)) {
if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() {
- if !signing_session.has_holder_tx_signatures() {
+ if !signing_session.has_holder_witnesses() {
// If we're a V1 channel or we haven't yet sent our `tx_signatures` for a dual
// funded channel, the funding tx couldn't be broadcasted yet, so just short-circuit
// the shutdown logic.
@@ -12919,7 +12974,7 @@ where
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
- .has_holder_tx_signatures();
+ .has_holder_witnesses();
if already_signed {
return Err(APIError::APIMisuseError {
err: format!(
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index db64cc9..1fc8a71 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11106,13 +11106,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
TransactionType::Funding { channels: vec![(counterparty_node_id, channel.context.channel_id())] },
)]);
}
- } else if let Some((splice_tx, tx_type)) = funding_tx_signed
+ } else if let Some((tx, tx_type)) = funding_tx_signed
.as_mut()
.and_then(|v| v.funding_tx.take())
.filter(|(_, tx_type)| matches!(tx_type, TransactionType::InteractiveFunding { .. }))
{
- log_info!(logger, "Broadcasting signed splice transaction with txid {}", splice_tx.compute_txid());
- self.tx_broadcaster.broadcast_transactions(&[(&splice_tx, tx_type)]);
+ log_info!(logger, "Broadcasting interactively funded transaction with txid {}", tx.compute_txid());
+ self.tx_broadcaster.broadcast_transactions(&[(&tx, tx_type)]);
}
{
@@ -13872,20 +13872,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
/// [`ChannelSigner`]: crate::sign::ChannelSigner
pub fn signer_unblocked(&self, channel_opt: Option<(PublicKey, ChannelId)>) {
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
+ let mut needs_holding_cell_release = false;
// Returns whether we should remove this channel as it's just been closed.
let unblock_chan = |chan: &mut Channel<SP>,
- pending_msg_events: &mut Vec<MessageSendEvent>|
+ pending_msg_events: &mut Vec<MessageSendEvent>,
+ needs_holding_cell_release: &mut bool|
-> Result<Option<ShutdownResult>, ChannelError> {
let channel_id = chan.context().channel_id();
let outbound_scid_alias = chan.context().outbound_scid_alias();
let logger = WithChannelContext::from(&self.logger, &chan.context(), None);
let node_id = chan.context().get_counterparty_node_id();
+ let best_block_height = self.best_block.read().unwrap().height;
let cbp = |htlc_id| {
self.path_for_release_held_htlc(htlc_id, outbound_scid_alias, &channel_id, &node_id)
};
- let msgs = chan.signer_maybe_unblocked(self.chain_hash, &&logger, cbp)?;
- if let Some(msgs) = msgs {
+ let msgs =
+ chan.signer_maybe_unblocked(self.chain_hash, best_block_height, &&logger, cbp)?;
+ if let Some(mut msgs) = msgs {
if chan.context().is_connected() {
if let Some(msg) = msgs.open_channel {
pending_msg_events.push(MessageSendEvent::SendOpenChannel { node_id, msg });
@@ -13925,7 +13929,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
pending_msg_events
.push(MessageSendEvent::SendFundingSigned { node_id, msg });
}
- if let Some(msg) = msgs.funding_commit_sig {
+ if let Some(msg) = msgs
+ .funding_tx_signed
+ .as_mut()
+ .and_then(|funding_tx_signed| funding_tx_signed.commitment_signed.take())
+ {
pending_msg_events.push(MessageSendEvent::UpdateHTLCs {
node_id,
channel_id,
@@ -13939,7 +13947,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
});
}
- if let Some(msg) = msgs.tx_signatures {
+ if let Some(msg) = msgs
+ .funding_tx_signed
+ .as_mut()
+ .and_then(|funding_tx_signed| funding_tx_signed.tx_signatures.take())
+ {
pending_msg_events
.push(MessageSendEvent::SendTxSignatures { node_id, msg });
}
@@ -13952,6 +13964,55 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
if let Some(msg) = msgs.channel_ready {
self.send_channel_ready(pending_msg_events, funded_chan, msg);
}
+ debug_assert!(msgs
+ .funding_tx_signed
+ .as_ref()
+ .and_then(|funding_tx_signed| {
+ funding_tx_signed.counterparty_initial_commitment_signed_result.as_ref()
+ })
+ .is_none());
+ if let Some(msg) = msgs
+ .funding_tx_signed
+ .as_mut()
+ .and_then(|funding_tx_signed| funding_tx_signed.splice_locked.take())
+ {
+ pending_msg_events
+ .push(MessageSendEvent::SendSpliceLocked { node_id, msg });
+ }
+ if let Some((tx, tx_type)) = msgs
+ .funding_tx_signed
+ .as_mut()
+ .and_then(|funding_tx_signed| funding_tx_signed.funding_tx.take())
+ {
+ debug_assert!(matches!(
+ tx_type,
+ TransactionType::InteractiveFunding { .. }
+ ));
+ log_info!(
+ logger,
+ "Broadcasting interactively funded transaction with txid {}",
+ tx.compute_txid(),
+ );
+ self.tx_broadcaster.broadcast_transactions(&[(&tx, tx_type)]);
+ }
+ if let Some(splice_negotiated) = msgs
+ .funding_tx_signed
+ .as_mut()
+ .and_then(|funding_tx_signed| funding_tx_signed.splice_negotiated.take())
+ {
+ *needs_holding_cell_release = true;
+ self.pending_events.lock().unwrap().push_back((
+ events::Event::SpliceNegotiated {
+ channel_id,
+ counterparty_node_id: node_id,
+ user_channel_id: funded_chan.context.get_user_id(),
+ new_funding_txo: splice_negotiated.funding_txo,
+ channel_type: splice_negotiated.channel_type,
+ new_funding_redeem_script: splice_negotiated.funding_redeem_script,
+ },
+ None,
+ ));
+ }
if let Some(broadcast_tx) = msgs.signed_closing_tx {
log_info!(logger, "Broadcasting closing tx {}", log_tx!(broadcast_tx));
self.tx_broadcaster.broadcast_transactions(&[(
@@ -13966,6 +14027,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
// We don't know how to handle a channel_ready or signed_closing_tx for a
// non-funded channel.
debug_assert!(msgs.channel_ready.is_none());
+ debug_assert!(msgs.funding_tx_signed.is_none());
debug_assert!(msgs.signed_closing_tx.is_none());
}
Ok(msgs.shutdown_result)
@@ -13989,7 +14051,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
peer_state.channel_by_id.retain(|_, chan| {
let shutdown_result = match channel_opt {
Some((_, channel_id)) if chan.context().channel_id() != channel_id => None,
- _ => match unblock_chan(chan, &mut peer_state.pending_msg_events) {
+ _ => match unblock_chan(
+ chan,
+ &mut peer_state.pending_msg_events,
+ &mut needs_holding_cell_release,
+ ) {
Ok(shutdown_result) => shutdown_result,
Err(err) => {
let (_, err) = self.locked_handle_force_close(
@@ -14030,6 +14096,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
});
}
drop(per_peer_state);
+ if needs_holding_cell_release {
+ self.check_free_holding_cells();
+ }
for (err, counterparty_node_id) in shutdown_results {
let _ = self.handle_error(err, counterparty_node_id);
}
@@ -20188,7 +20257,7 @@ impl<
if let Some(signing_session) =
chan.context().interactive_tx_signing_session.as_ref()
{
- if !signing_session.has_holder_tx_signatures()
+ if !signing_session.has_holder_witnesses()
&& signing_session.has_local_contribution()
{
let unsigned_transaction = signing_session.unsigned_tx().tx().clone();
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 6396298..faa352f 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -18,6 +18,7 @@ use bitcoin::constants::WITNESS_SCALE_FACTOR;
use bitcoin::ecdsa::Signature as BitcoinSignature;
use bitcoin::key::Secp256k1;
use bitcoin::policy::MAX_STANDARD_TX_WEIGHT;
+use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::{Message, PublicKey};
use bitcoin::sighash::SighashCache;
use bitcoin::transaction::Version;
@@ -432,12 +433,12 @@ impl ConstructedTransaction {
}
fn finalize(
- &self, holder_tx_signatures: &TxSignatures, counterparty_tx_signatures: &TxSignatures,
- shared_input_sig: Option<&SharedInputSignature>,
+ &self, holder_tx_signatures: TxSignatures, counterparty_tx_signatures: TxSignatures,
+ shared_input_sig: Option<SharedInputSignature>,
) -> Option<Transaction> {
let mut tx = self.tx.clone();
- self.add_local_witnesses(&mut tx, holder_tx_signatures.witnesses.clone());
- self.add_remote_witnesses(&mut tx, counterparty_tx_signatures.witnesses.clone());
+ self.add_local_witnesses(&mut tx, holder_tx_signatures.witnesses);
+ self.add_remote_witnesses(&mut tx, counterparty_tx_signatures.witnesses);
if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
@@ -568,13 +569,25 @@ impl InteractiveTxSigningSession {
self.counterparty_tx_signatures.is_some()
}
- pub fn has_holder_tx_signatures(&self) -> bool {
+ pub fn has_holder_witnesses(&self) -> bool {
self.holder_tx_signatures.is_some()
}
+ pub fn awaiting_holder_shared_input_signature(&self) -> bool {
+ self.holder_tx_signatures
+ .as_ref()
+ .map(|tx_signatures| {
+ self.shared_input().is_some() && tx_signatures.shared_input_signature.is_none()
+ })
+ .unwrap_or(false)
+ }
+
pub fn holder_tx_signatures(&self) -> Option<TxSignatures> {
self.holder_tx_signatures
.as_ref()
+ .filter(|tx_signatures| {
+ self.shared_input().is_none() || tx_signatures.shared_input_signature.is_some()
+ })
.filter(|_| {
(self.has_received_commitment_signed && self.holder_sends_tx_signatures_first)
|| self.has_received_tx_signatures()
@@ -615,51 +628,78 @@ impl InteractiveTxSigningSession {
self.counterparty_tx_signatures = Some(tx_signatures.clone());
- let holder_tx_signatures = if !self.holder_sends_tx_signatures_first {
- self.holder_tx_signatures.clone()
- } else {
- None
- };
+ let holder_tx_signatures =
+ if !self.holder_sends_tx_signatures_first { self.holder_tx_signatures() } else { None };
let funding_tx_opt = self.signed_tx();
Ok((holder_tx_signatures, funding_tx_opt))
}
- /// Provides the holder witnesses for the unsigned transaction.
+ /// Provides the holder witnesses for the unsigned transaction's non-shared inputs.
+ ///
+ /// For splices, call [`Self::provide_holder_shared_input_signature`] separately after the
+ /// shared input signature is available.
///
/// Returns an error if the witness count does not equal the holder's input count in the
/// unsigned transaction.
pub fn provide_holder_witnesses<C: bitcoin::secp256k1::Verification>(
- &mut self, tx_signatures: TxSignatures, secp_ctx: &Secp256k1<C>,
+ &mut self, channel_id: ChannelId, funding_txid_signed: Txid, witnesses: Vec<Witness>,
+ secp_ctx: &Secp256k1<C>,
) -> Result<(Option<TxSignatures>, Option<Transaction>), String> {
if self.holder_tx_signatures.is_some() {
return Err("Holder witnesses were already provided".to_string());
}
+ if funding_txid_signed != self.unsigned_tx().compute_txid() {
+ return Err("Transaction was malleated prior to signing".to_string());
+ }
+
let local_inputs_count = self.local_inputs_count();
- if tx_signatures.witnesses.len() != local_inputs_count {
+ if witnesses.len() != local_inputs_count {
return Err(format!(
"Provided witness count of {} does not match required count for {} non-shared inputs",
- tx_signatures.witnesses.len(),
+ witnesses.len(),
local_inputs_count
));
}
- self.verify_interactive_tx_signatures(secp_ctx, &tx_signatures.witnesses)?;
+ self.verify_interactive_tx_signatures(secp_ctx, &witnesses)?;
- self.holder_tx_signatures = Some(tx_signatures);
+ self.holder_tx_signatures = Some(TxSignatures {
+ channel_id,
+ tx_hash: funding_txid_signed,
+ witnesses,
+ shared_input_signature: None,
+ });
+ let holder_tx_signatures = self.holder_tx_signatures();
let funding_tx_opt = self.signed_tx();
- let holder_tx_signatures = (self.has_received_commitment_signed
- && (self.holder_sends_tx_signatures_first || self.has_received_tx_signatures()))
- .then(|| {
- self.holder_tx_signatures.clone().expect("Holder tx_signatures were just provided")
- });
Ok((holder_tx_signatures, funding_tx_opt))
}
+ pub fn provide_holder_shared_input_signature(
+ &mut self, shared_input_signature: Signature,
+ ) -> Result<(Option<TxSignatures>, Option<Transaction>), String> {
+ if self.shared_input().is_none() {
+ return Err("No shared input exists for this transaction".to_string());
+ }
+
+ let holder_tx_signatures = self.holder_tx_signatures.as_mut().ok_or_else(|| {
+ "Holder witnesses must be provided before the shared input signature".to_string()
+ })?;
+ if holder_tx_signatures.shared_input_signature.is_some() {
+ return Err("The shared input signature was already provided".to_string());
+ }
+
+ holder_tx_signatures.shared_input_signature = Some(shared_input_signature);
+
+ let funding_tx_opt = self.signed_tx();
+ let holder_tx_signatures = self.holder_tx_signatures();
+ Ok((holder_tx_signatures, funding_tx_opt))
+ }
+
pub fn remote_inputs_count(&self) -> usize {
let shared_index = self.unsigned_tx.shared_input_index.as_ref();
self.unsigned_tx
@@ -710,9 +750,9 @@ impl InteractiveTxSigningSession {
/// Returns `Some` with the fully signed transaction if both holder and counterparty signatures
/// are available.
pub fn signed_tx(&self) -> Option<Transaction> {
- let holder_tx_signatures = self.holder_tx_signatures.as_ref()?;
- let counterparty_tx_signatures = self.counterparty_tx_signatures.as_ref()?;
- let shared_input_signature = self.shared_input_signature.as_ref();
+ let holder_tx_signatures = self.holder_tx_signatures()?;
+ let counterparty_tx_signatures = self.counterparty_tx_signatures.clone()?;
+ let shared_input_signature = self.shared_input_signature.clone();
self.unsigned_tx.finalize(
holder_tx_signatures,
counterparty_tx_signatures,
diff --git a/lightning/src/sign/ecdsa.rs b/lightning/src/sign/ecdsa.rs
index e132857..c0bd375 100644
--- a/lightning/src/sign/ecdsa.rs
+++ b/lightning/src/sign/ecdsa.rs
@@ -254,8 +254,14 @@ pub trait EcdsaChannelSigner: ChannelSigner {
///
/// `input_index`: The index of the input within the new funding transaction `tx`,
/// spending the previous funding transaction's output
+ ///
+ /// An `Err` can be returned to signal that the signer is unavailable/cannot produce a valid
+ /// signature and should be retried later. Once the signer is ready to provide a signature after
+ /// previously returning an `Err`, [`ChannelManager::signer_unblocked`] must be called.
+ ///
+ /// [`ChannelManager::signer_unblocked`]: crate::ln::channelmanager::ChannelManager::signer_unblocked
fn sign_splice_shared_input(
&self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction,
input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>,
- ) -> Signature;
+ ) -> Result<Signature, ()>;
}
diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 374ad38..a3dc720 100644
--- a/lightning/src/sign/mod.rs
+++ b/lightning/src/sign/mod.rs
@@ -1928,7 +1928,7 @@ impl EcdsaChannelSigner for InMemorySigner {
fn sign_splice_shared_input(
&self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction,
input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>,
- ) -> Signature {
+ ) -> Result<Signature, ()> {
assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated");
assert_eq!(
tx.input[input_index].previous_output,
@@ -1954,7 +1954,7 @@ impl EcdsaChannelSigner for InMemorySigner {
)
.unwrap()[..];
let msg = hash_to_message!(sighash);
- sign(secp_ctx, &msg, &funding_key)
+ Ok(sign(secp_ctx, &msg, &funding_key))
}
}
diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs
index 436eaab..5da284d 100644
--- a/lightning/src/util/dyn_signer.rs
+++ b/lightning/src/util/dyn_signer.rs
@@ -90,7 +90,7 @@ delegate!(DynSigner, EcdsaChannelSigner, inner,
fn sign_holder_htlc_transaction(, htlc_tx: &Transaction, input: usize,
htlc_descriptor: &HTLCDescriptor, secp_ctx: &Secp256k1<All>) -> Result<Signature, ()>,
fn sign_splice_shared_input(, channel_parameters: &ChannelTransactionParameters,
- tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<All>) -> Signature
+ tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<All>) -> Result<Signature, ()>
);
delegate!(DynSigner, ChannelSigner,
diff --git a/lightning/src/util/test_channel_signer.rs b/lightning/src/util/test_channel_signer.rs
index 8435e7f..668bbeb 100644
--- a/lightning/src/util/test_channel_signer.rs
+++ b/lightning/src/util/test_channel_signer.rs
@@ -103,6 +103,7 @@ pub enum SignerOp {
SignClosingTransaction,
SignHolderAnchorInput,
SignChannelAnnouncementWithFundingKey,
+ SignSpliceSharedInput,
}
impl SignerOp {
@@ -120,6 +121,7 @@ impl SignerOp {
SignerOp::SignClosingTransaction,
SignerOp::SignHolderAnchorInput,
SignerOp::SignChannelAnnouncementWithFundingKey,
+ SignerOp::SignSpliceSharedInput,
]
}
}
@@ -507,7 +509,11 @@ impl EcdsaChannelSigner for TestChannelSigner {
fn sign_splice_shared_input(
&self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction,
input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>,
- ) -> Signature {
+ ) -> Result<Signature, ()> {
+ #[cfg(any(test, feature = "_test_utils"))]
+ if !self.is_signer_available(SignerOp::SignSpliceSharedInput) {
+ return Err(());
+ }
self.inner.sign_splice_shared_input(channel_parameters, tx, input_index, secp_ctx)
}
}
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.