Introduce `FundingTransactionReadyForSignatures` event
What changed, and why it matters
This commit adds a new event that asks the wallet/user to sign inputs they contributed to a jointly-built Lightning channel funding transaction. It also changes when LDK sends its own signatures so that signatures are only provided after the channel monitor has been safely persisted. The change is a feature addition with safety improvements, not a fix for an active bug or known exploit.
Review the new `funding_transaction_signed` API and event handling in downstream integrations; ensure wallet code only signs the exact `unsigned_transaction` provided and uses `SIGHASH_ALL`. No urgent patch is required, but monitor for follow-up commits that complete async-signing TODOs.
Security signals we found
New event-driven signing flow for holder-contributed inputs
Removal of monitor_pending_tx_signatures field and deferred-signature logic
Signatures now gated on monitor persistence via handle_channel_resumption
Addition of has_received_tx_signatures to reject duplicate tx_signatures messages
SIGHASH_ALL requirement documented for contributed input signatures
Warning about counterparty non-standard inputs documented
Evidence from the diff
The patch introduces Event::FundingTransactionReadyForSigning and a new public API ChannelManager::funding_transaction_signed. It removes the old monitor_pending_tx_signatures mechanism and instead queues the signing event only inside handle_channel_resumption, which runs after the commitment-signed monitor update completes. This ensures holder signatures are never sent before the monitor is persisted. It also adds a has_received_tx_signatures flag to prevent duplicate tx_signatures messages and makes provide_holder_witnesses return the finalized transaction and holder tx_signatures when appropriate. The commit is part of interactive transaction construction (dual-funding/splicing support).
Changed components
lightning/src/events/mod.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/interactivetxs.rsInspect captured patch +291 / −140
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 8ac0230..0cf5131 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1698,6 +1698,52 @@ pub enum Event {
/// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice
reply_path: Responder,
},
+ /// Indicates that a channel funding transaction constructed interactively is ready to be
+ /// signed. This event will only be triggered if at least one input was contributed.
+ ///
+ /// The transaction contains all inputs and outputs provided by both parties including the
+ /// channel's funding output and a change output if applicable.
+ ///
+ /// No part of the transaction should be changed before signing as the content of the transaction
+ /// has already been negotiated with the counterparty.
+ ///
+ /// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
+ /// hence possible loss of funds.
+ ///
+ /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
+ /// funding transaction.
+ ///
+ /// Generated in [`ChannelManager`] message handling.
+ ///
+ /// # Failure Behavior and Persistence
+ /// This event will eventually be replayed after failures-to-handle (i.e., the event handler
+ /// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
+ ///
+ /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+ /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
+ FundingTransactionReadyForSigning {
+ /// The `channel_id` of the channel which you'll need to pass back into
+ /// [`ChannelManager::funding_transaction_signed`].
+ ///
+ /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
+ channel_id: ChannelId,
+ /// The counterparty's `node_id`, which you'll need to pass back into
+ /// [`ChannelManager::funding_transaction_signed`].
+ ///
+ /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
+ counterparty_node_id: PublicKey,
+ /// The `user_channel_id` value passed in for outbound channels, or for inbound channels if
+ /// [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. Otherwise
+ /// `user_channel_id` will be randomized for inbound channels.
+ ///
+ /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels
+ user_channel_id: u128,
+ /// The unsigned transaction to be signed and passed back to
+ /// [`ChannelManager::funding_transaction_signed`].
+ ///
+ /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
+ unsigned_transaction: Transaction,
+ },
}
impl Writeable for Event {
@@ -2140,6 +2186,11 @@ impl Writeable for Event {
47u8.write(writer)?;
// Never write StaticInvoiceRequested events as buffered onion messages aren't serialized.
},
+ &Event::FundingTransactionReadyForSigning { .. } => {
+ 49u8.write(writer)?;
+ // We never write out FundingTransactionReadyForSigning events as they will be regenerated when
+ // necessary.
+ },
// Note that, going forward, all new events must only write data inside of
// `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write
// data via `write_tlv_fields`.
@@ -2722,6 +2773,8 @@ impl MaybeReadable for Event {
// Note that we do not write a length-prefixed TLV for StaticInvoiceRequested events.
#[cfg(async_payments)]
47u8 => Ok(None),
+ // Note that we do not write a length-prefixed TLV for FundingTransactionReadyForSigning events.
+ 49u8 => Ok(None),
// Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue.
// Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt
// reads.
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index bd4cdcd..b65ef44 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -14,7 +14,7 @@ use bitcoin::constants::ChainHash;
use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash};
use bitcoin::sighash::EcdsaSighashType;
use bitcoin::transaction::{Transaction, TxIn, TxOut};
-use bitcoin::Weight;
+use bitcoin::{Weight, Witness};
use bitcoin::hash_types::{BlockHash, Txid};
use bitcoin::hashes::sha256::Hash as Sha256;
@@ -24,9 +24,9 @@ use bitcoin::hashes::Hash;
use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE;
use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1};
use bitcoin::secp256k1::{PublicKey, SecretKey};
-use bitcoin::{secp256k1, sighash};
#[cfg(splicing)]
-use bitcoin::{Sequence, Witness};
+use bitcoin::Sequence;
+use bitcoin::{secp256k1, sighash};
use crate::chain::chaininterface::{
fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator,
@@ -38,7 +38,7 @@ use crate::chain::channelmonitor::{
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::chain::BestBlock;
use crate::events::bump_transaction::BASE_INPUT_WEIGHT;
-use crate::events::{ClosureReason, Event};
+use crate::events::ClosureReason;
use crate::ln::chan_utils;
#[cfg(splicing)]
use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
@@ -1774,7 +1774,7 @@ where
pub fn funding_tx_constructed<L: Deref>(
&mut self, logger: &L,
- ) -> Result<(msgs::CommitmentSigned, Option<Event>), msgs::TxAbort>
+ ) -> Result<msgs::CommitmentSigned, msgs::TxAbort>
where
L::Target: Logger,
{
@@ -1786,7 +1786,7 @@ where
.take()
.expect("PendingV2Channel::interactive_tx_constructor should be set")
.into_signing_session();
- let (commitment_signed, event) = chan.context.funding_tx_constructed(
+ let commitment_signed = chan.context.funding_tx_constructed(
&mut chan.funding,
&mut signing_session,
false,
@@ -1796,7 +1796,7 @@ where
chan.interactive_tx_signing_session = Some(signing_session);
- return Ok((commitment_signed, event));
+ return Ok(commitment_signed);
},
#[cfg(splicing)]
ChannelPhase::Funded(chan) => {
@@ -1809,7 +1809,7 @@ where
{
let mut signing_session =
interactive_tx_constructor.into_signing_session();
- let (commitment_signed, event) = chan.context.funding_tx_constructed(
+ let commitment_signed = chan.context.funding_tx_constructed(
&mut funding,
&mut signing_session,
true,
@@ -1821,7 +1821,7 @@ where
pending_splice.funding_negotiation =
Some(FundingNegotiation::AwaitingSignatures(funding));
- return Ok((commitment_signed, event));
+ return Ok(commitment_signed);
} else {
// Replace the taken state
pending_splice.funding_negotiation = Some(funding_negotiation);
@@ -2539,7 +2539,6 @@ where
monitor_pending_failures: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
monitor_pending_finalized_fulfills: Vec<(HTLCSource, Option<AttributionData>)>,
monitor_pending_update_adds: Vec<msgs::UpdateAddHTLC>,
- monitor_pending_tx_signatures: Option<msgs::TxSignatures>,
/// If we went to send a revoke_and_ack but our signer was unable to give us a signature,
/// we should retry at some point in the future when the signer indicates it may have a
@@ -3259,7 +3258,6 @@ where
monitor_pending_failures: Vec::new(),
monitor_pending_finalized_fulfills: Vec::new(),
monitor_pending_update_adds: Vec::new(),
- monitor_pending_tx_signatures: None,
signer_pending_revoke_and_ack: false,
signer_pending_commitment_update: false,
@@ -3498,7 +3496,6 @@ where
monitor_pending_failures: Vec::new(),
monitor_pending_finalized_fulfills: Vec::new(),
monitor_pending_update_adds: Vec::new(),
- monitor_pending_tx_signatures: None,
signer_pending_revoke_and_ack: false,
signer_pending_commitment_update: false,
@@ -5523,7 +5520,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>), msgs::TxAbort>
+ ) -> Result<msgs::CommitmentSigned, msgs::TxAbort>
where
L::Target: Logger
{
@@ -5581,44 +5578,7 @@ where
},
};
- let funding_ready_for_sig_event = if signing_session.local_inputs_count() == 0 {
- if signing_session.provide_holder_witnesses(self.channel_id, Vec::new()).is_err() {
- debug_assert!(
- false,
- "Zero inputs were provided & zero witnesses were provided, but a count mismatch was somehow found",
- );
- return Err(msgs::TxAbort {
- channel_id: self.channel_id(),
- data: "V2 channel rejected due to sender error".to_owned().into_bytes(),
- });
- }
- None
- } else {
- // TODO(dual_funding): Send event for signing if we've contributed funds.
- // Inform the user that SIGHASH_ALL must be used for all signatures when contributing
- // inputs/signatures.
- // Also warn the user that we don't do anything to prevent the counterparty from
- // providing non-standard witnesses which will prevent the funding transaction from
- // confirming. This warning must appear in doc comments wherever the user is contributing
- // funds, whether they are initiator or acceptor.
- //
- // The following warning can be used when the APIs allowing contributing inputs become available:
- // <div class="warning">
- // WARNING: LDK makes no attempt to prevent the counterparty from using non-standard inputs which
- // will prevent the funding transaction from being relayed on the bitcoin network and hence being
- // confirmed.
- // </div>
- debug_assert!(
- false,
- "We don't support users providing inputs but somehow we had more than zero inputs",
- );
- return Err(msgs::TxAbort {
- channel_id: self.channel_id(),
- data: "V2 channel rejected due to sender error".to_owned().into_bytes(),
- });
- };
-
- Ok((commitment_signed, funding_ready_for_sig_event))
+ Ok(commitment_signed)
}
/// Asserts that the commitment tx numbers have not advanced from their initial number.
@@ -7017,15 +6977,7 @@ where
log_info!(logger, "Received initial commitment_signed from peer for channel {}", &self.context.channel_id());
self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new());
-
- if let Some(tx_signatures) = self.interactive_tx_signing_session.as_mut().and_then(
- |session| session.received_commitment_signed()
- ) {
- // We're up first for submitting our tx_signatures, but our monitor has not persisted yet
- // so they'll be sent as soon as that's done.
- self.context.monitor_pending_tx_signatures = Some(tx_signatures);
- }
-
+ self.interactive_tx_signing_session.as_mut().expect("signing session should be present").received_commitment_signed();
Ok(channel_monitor)
}
@@ -7111,13 +7063,11 @@ where
channel_id: Some(self.context.channel_id()),
};
- let tx_signatures = self
- .interactive_tx_signing_session
+ self.interactive_tx_signing_session
.as_mut()
.expect("Signing session must exist for negotiated pending splice")
.received_commitment_signed();
self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new());
- self.context.monitor_pending_tx_signatures = tx_signatures;
Ok(self.push_ret_blockable_mon_update(monitor_update))
}
@@ -8027,10 +7977,39 @@ where
}
}
+ pub fn funding_transaction_signed(
+ &mut self, witnesses: Vec<Witness>,
+ ) -> Result<(Option<msgs::TxSignatures>, Option<Transaction>), APIError> {
+ let (funding_tx_opt, tx_signatures_opt) = self
+ .interactive_tx_signing_session
+ .as_mut()
+ .ok_or_else(|| APIError::APIMisuseError {
+ err: format!(
+ "Channel {} not expecting funding signatures",
+ self.context.channel_id
+ ),
+ })
+ .and_then(|signing_session| {
+ signing_session
+ .provide_holder_witnesses(self.context.channel_id, witnesses)
+ .map_err(|err| APIError::APIMisuseError { err })
+ })?;
+
+ if tx_signatures_opt.is_some() {
+ self.context.channel_state.set_our_tx_signatures_ready();
+ }
+
+ if funding_tx_opt.is_some() {
+ self.funding.funding_transaction = funding_tx_opt.clone();
+ self.context.channel_state =
+ ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new());
+ }
+
+ Ok((tx_signatures_opt, funding_tx_opt))
+ }
+
#[rustfmt::skip]
- pub fn tx_signatures<L: Deref>(&mut self, msg: &msgs::TxSignatures, logger: &L) -> Result<(Option<Transaction>, Option<msgs::TxSignatures>), ChannelError>
- where L::Target: Logger
- {
+ pub fn tx_signatures(&mut self, msg: &msgs::TxSignatures) -> Result<(Option<Transaction>, Option<msgs::TxSignatures>), ChannelError> {
if !self.context.channel_state.is_interactive_signing()
|| self.context.channel_state.is_their_tx_signatures_sent()
{
@@ -8053,50 +8032,29 @@ where
return Err(ChannelError::Close((msg.to_owned(), reason)));
}
- if msg.witnesses.len() != signing_session.remote_inputs_count() {
- return Err(ChannelError::Warn(
- "Witness count did not match contributed input count".to_string()
- ));
- }
-
for witness in &msg.witnesses {
if witness.is_empty() {
let msg = "Unexpected empty witness in tx_signatures received";
let reason = ClosureReason::ProcessingError { err: msg.to_owned() };
return Err(ChannelError::Close((msg.to_owned(), reason)));
}
-
- // TODO(dual_funding): Check all sigs are SIGHASH_ALL.
-
- // TODO(dual_funding): I don't see how we're going to be able to ensure witness-standardness
- // for spending. Doesn't seem to be anything in rust-bitcoin.
}
let (holder_tx_signatures_opt, funding_tx_opt) = signing_session.received_tx_signatures(msg.clone())
- .map_err(|_| ChannelError::Warn("Witness count did not match contributed input count".to_string()))?;
+ .map_err(|msg| ChannelError::Warn(msg))?;
// Set `THEIR_TX_SIGNATURES_SENT` flag after all potential errors.
self.context.channel_state.set_their_tx_signatures_sent();
if funding_tx_opt.is_some() {
+ // TODO(splicing): Transition back to `ChannelReady` and not `AwaitingChannelReady`
+ // We will also need to use the pending `FundingScope` in the splicing case.
+ //
// We have a finalized funding transaction, so we can set the funding transaction.
self.funding.funding_transaction = funding_tx_opt.clone();
+ self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new());
}
- // Note that `holder_tx_signatures_opt` will be `None` if we sent `tx_signatures` first, so this
- // case checks if there is a monitor persist in progress when we need to respond with our `tx_signatures`
- // and sets it as pending.
- if holder_tx_signatures_opt.is_some() && self.is_awaiting_initial_mon_persist() {
- log_debug!(logger, "Not sending tx_signatures: a monitor update is in progress. Setting monitor_pending_tx_signatures.");
- self.context.monitor_pending_tx_signatures = holder_tx_signatures_opt;
- return Ok((None, None));
- }
-
- if holder_tx_signatures_opt.is_some() {
- self.context.channel_state.set_our_tx_signatures_ready();
- }
-
- self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new());
Ok((funding_tx_opt, holder_tx_signatures_opt))
} else {
let msg = "Unexpected tx_signatures. No funding transaction awaiting signatures";
@@ -8348,17 +8306,6 @@ where
mem::swap(&mut finalized_claimed_htlcs, &mut self.context.monitor_pending_finalized_fulfills);
let mut pending_update_adds = Vec::new();
mem::swap(&mut pending_update_adds, &mut self.context.monitor_pending_update_adds);
- // For channels established with V2 establishment we won't send a `tx_signatures` when we're in
- // MonitorUpdateInProgress (and we assume the user will never directly broadcast the funding
- // transaction and waits for us to do it).
- let tx_signatures = self.context.monitor_pending_tx_signatures.take();
- if tx_signatures.is_some() {
- if self.context.channel_state.is_their_tx_signatures_sent() {
- self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new());
- } else {
- self.context.channel_state.set_our_tx_signatures_ready();
- }
- }
if self.context.channel_state.is_peer_disconnected() {
self.context.monitor_pending_revoke_and_ack = false;
@@ -8366,7 +8313,7 @@ where
return MonitorRestoreUpdates {
raa: None, commitment_update: None, order: RAACommitmentOrder::RevokeAndACKFirst,
accepted_htlcs, failed_htlcs, finalized_claimed_htlcs, pending_update_adds,
- funding_broadcastable, channel_ready, announcement_sigs, tx_signatures
+ funding_broadcastable, channel_ready, announcement_sigs, tx_signatures: None
};
}
@@ -8396,7 +8343,7 @@ where
match order { RAACommitmentOrder::CommitmentFirst => "commitment", RAACommitmentOrder::RevokeAndACKFirst => "RAA"});
MonitorRestoreUpdates {
raa, commitment_update, order, accepted_htlcs, failed_htlcs, finalized_claimed_htlcs,
- pending_update_adds, funding_broadcastable, channel_ready, announcement_sigs, tx_signatures
+ pending_update_adds, funding_broadcastable, channel_ready, announcement_sigs, tx_signatures: None
}
}
@@ -8882,7 +8829,6 @@ where
update_fee: None,
})
} else { None };
- // TODO(dual_funding): For async signing support we need to hold back `tx_signatures` until the `commitment_signed` is ready.
let tx_signatures = if (
// if it has not received tx_signatures for that funding transaction AND
// if it has already received commitment_signed AND it should sign first, as specified in the tx_signatures requirements:
@@ -8891,19 +8837,13 @@ where
// else if it has already received tx_signatures for that funding transaction:
// MUST send its tx_signatures for that funding transaction.
) || self.context.channel_state.is_their_tx_signatures_sent() {
- if self.context.channel_state.is_monitor_update_in_progress() {
- // The `monitor_pending_tx_signatures` field should have already been set in `commitment_signed_initial_v2`
- // if we were up first for signing and had a monitor update in progress, but check again just in case.
- debug_assert!(self.context.monitor_pending_tx_signatures.is_some(), "monitor_pending_tx_signatures should already be set");
- log_debug!(logger, "Not sending tx_signatures: a monitor update is in progress. Setting monitor_pending_tx_signatures.");
- if self.context.monitor_pending_tx_signatures.is_none() {
- self.context.monitor_pending_tx_signatures = session.holder_tx_signatures().clone();
- }
+ // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent
+ // when the holder provides their witnesses as this will queue a `tx_signatures` if the
+ // holder must send one.
+ if session.holder_tx_signatures().is_none() {
+ log_debug!(logger, "Waiting for funding transaction signatures to be provided");
None
} else {
- // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent
- // when the holder provides their witnesses as this will queue a `tx_signatures` if the
- // holder must send one.
session.holder_tx_signatures().clone()
}
} else {
@@ -14005,7 +13945,6 @@ where
monitor_pending_failures,
monitor_pending_finalized_fulfills: monitor_pending_finalized_fulfills.unwrap(),
monitor_pending_update_adds: monitor_pending_update_adds.unwrap_or_default(),
- monitor_pending_tx_signatures: None,
signer_pending_revoke_and_ack: false,
signer_pending_commitment_update: false,
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 455f3e9..aeecbb4 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -5925,6 +5925,113 @@ where
result
}
+ /// Handles a signed funding transaction generated by interactive transaction construction and
+ /// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`]
+ /// event.
+ ///
+ /// Do NOT broadcast the funding transaction yourself. When we have safely received our
+ /// counterparty's signature(s) the funding transaction will automatically be broadcast via the
+ /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed.
+ ///
+ /// `SIGHASH_ALL` MUST be used for all signatures when providing signatures.
+ ///
+ /// <div class="warning">
+ /// WARNING: LDK makes no attempt to prevent the counterparty from using non-standard inputs which
+ /// will prevent the funding transaction from being relayed on the bitcoin network and hence being
+ /// confirmed.
+ /// </div>
+ ///
+ /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
+ /// `counterparty_node_id` is provided.
+ ///
+ /// Returns [`APIMisuseError`] when a channel is not in a state where it is expecting funding
+ /// signatures.
+ ///
+ /// [`FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning
+ /// [`ChannelUnavailable`]: APIError::ChannelUnavailable
+ /// [`APIMisuseError`]: APIError::APIMisuseError
+ pub fn funding_transaction_signed(
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction,
+ ) -> Result<(), APIError> {
+ let mut result = Ok(());
+ PersistenceNotifierGuard::optionally_notify(self, || {
+ let per_peer_state = self.per_peer_state.read().unwrap();
+ let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id);
+ if peer_state_mutex_opt.is_none() {
+ result = Err(APIError::ChannelUnavailable {
+ err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}")
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ }
+
+ let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap();
+
+ match peer_state.channel_by_id.get_mut(channel_id) {
+ Some(channel) => match channel.as_funded_mut() {
+ Some(chan) => {
+ let witnesses: Vec<_> = transaction
+ .input
+ .into_iter()
+ .map(|input| input.witness)
+ .filter(|witness| witness.is_empty())
+ .collect();
+ match chan.funding_transaction_signed(witnesses) {
+ Ok((Some(tx_signatures), funding_tx_opt)) => {
+ if let Some(funding_tx) = funding_tx_opt {
+ self.broadcast_interactive_funding(chan, &funding_tx);
+ }
+ peer_state.pending_msg_events.push(
+ MessageSendEvent::SendTxSignatures {
+ node_id: *counterparty_node_id,
+ msg: tx_signatures,
+ },
+ );
+ return NotifyOption::DoPersist;
+ },
+ Err(err) => {
+ result = Err(err);
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ _ => {
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ }
+ },
+ None => {
+ result = Err(APIError::APIMisuseError {
+ err: format!(
+ "Channel with id {} not expecting funding signatures",
+ channel_id
+ ),
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ },
+ None => {
+ result = Err(APIError::ChannelUnavailable {
+ err: format!(
+ "Channel with id {} not found for the passed counterparty node_id {}",
+ channel_id, counterparty_node_id
+ ),
+ });
+ return NotifyOption::SkipPersistNoEvents;
+ },
+ }
+ });
+
+ result
+ }
+
+ fn broadcast_interactive_funding(
+ &self, channel: &mut FundedChannel<SP>, funding_tx: &Transaction,
+ ) {
+ self.tx_broadcaster.broadcast_transactions(&[funding_tx]);
+ {
+ let mut pending_events = self.pending_events.lock().unwrap();
+ emit_channel_pending_event!(pending_events, channel);
+ }
+ }
+
/// Atomically applies partial updates to the [`ChannelConfig`] of the given channels.
///
/// Once the updates are applied, each eligible channel (advertised with a known short channel
@@ -8839,6 +8946,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
msg,
});
}
+ // TODO(dual_funding): For async signing support we need to hold back `tx_signatures` until the `commitment_signed` is ready.
if let Some(msg) = tx_signatures {
pending_msg_events.push(MessageSendEvent::SendTxSignatures {
node_id: counterparty_node_id,
@@ -8899,6 +9007,46 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
}
+ if let Some(signing_session) = &mut channel.interactive_tx_signing_session {
+ if signing_session.local_inputs_count() > 0
+ && signing_session.holder_tx_signatures().is_none()
+ {
+ let mut pending_events = self.pending_events.lock().unwrap();
+ let unsigned_transaction = signing_session.unsigned_tx().build_unsigned_tx();
+ let event_action = (
+ Event::FundingTransactionReadyForSigning {
+ unsigned_transaction,
+ counterparty_node_id,
+ channel_id: channel.context.channel_id(),
+ user_channel_id: channel.context.get_user_id(),
+ },
+ None,
+ );
+
+ if pending_events.contains(&event_action) {
+ debug_assert!(false, "FundingTransactionReadyForSigning should not have been queued already");
+ } else {
+ pending_events.push_back(event_action);
+ }
+ } else if signing_session.local_inputs_count() == 0 && signing_session.holder_tx_signatures().is_none() {
+ match channel.funding_transaction_signed(vec![]) {
+ Ok((Some(tx_signatures), funding_tx_opt)) => {
+ if let Some(funding_tx) = funding_tx_opt {
+ self.broadcast_interactive_funding(channel, &funding_tx);
+ }
+ pending_msg_events.push(MessageSendEvent::SendTxSignatures {
+ node_id: counterparty_node_id,
+ msg: tx_signatures,
+ });
+ },
+ Ok((None, _)) => {
+ debug_assert!(false, "If our tx_signatures is empty, then we should send it first!");
+ },
+ Err(err) => debug_assert!(false, "We should not error here but we got: {:?}", err),
+ }
+ }
+ }
+
{
let mut pending_events = self.pending_events.lock().unwrap();
emit_channel_pending_event!(pending_events, channel);
@@ -9774,11 +9922,11 @@ 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) = match chan_entry
+ let commitment_signed = match chan_entry
.get_mut()
.funding_tx_constructed(&self.logger)
{
- Ok((commitment_signed, event)) => (commitment_signed, event),
+ Ok(commitment_signed) => commitment_signed,
Err(tx_abort) => {
if chan_entry.get().is_funded() {
peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort {
@@ -9795,10 +9943,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
},
};
- 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));
- }
peer_state.pending_msg_events.push(MessageSendEvent::UpdateHTLCs {
node_id: counterparty_node_id,
channel_id: msg.channel_id,
@@ -9837,8 +9981,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
hash_map::Entry::Occupied(mut chan_entry) => {
match chan_entry.get_mut().as_funded_mut() {
Some(chan) => {
- let logger = WithChannelContext::from(&self.logger, &chan.context, None);
- let (funding_tx_opt, tx_signatures_opt) = try_channel_entry!(self, peer_state, chan.tx_signatures(msg, &&logger), chan_entry);
+ let (funding_tx_opt, tx_signatures_opt) = try_channel_entry!(self, peer_state, chan.tx_signatures(msg), chan_entry);
if let Some(tx_signatures) = tx_signatures_opt {
peer_state.pending_msg_events.push(MessageSendEvent::SendTxSignatures {
node_id: *counterparty_node_id,
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 3fcf3f4..5ed3d5e 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -375,6 +375,7 @@ pub(crate) struct InteractiveTxSigningSession {
unsigned_tx: ConstructedTransaction,
holder_sends_tx_signatures_first: bool,
has_received_commitment_signed: bool,
+ has_received_tx_signatures: bool,
holder_tx_signatures: Option<TxSignatures>,
}
@@ -395,13 +396,8 @@ impl InteractiveTxSigningSession {
&self.holder_tx_signatures
}
- pub fn received_commitment_signed(&mut self) -> Option<TxSignatures> {
+ pub fn received_commitment_signed(&mut self) {
self.has_received_commitment_signed = true;
- if self.holder_sends_tx_signatures_first {
- self.holder_tx_signatures.clone()
- } else {
- None
- }
}
/// Handles a `tx_signatures` message received from the counterparty.
@@ -414,14 +410,18 @@ impl InteractiveTxSigningSession {
/// transaction will be finalized and returned as Some, otherwise None.
///
/// Returns an error if the witness count does not equal the counterparty's input count in the
- /// unsigned transaction.
+ /// unsigned transaction or if the counterparty already provided their `tx_signatures`.
pub fn received_tx_signatures(
&mut self, tx_signatures: TxSignatures,
- ) -> Result<(Option<TxSignatures>, Option<Transaction>), ()> {
+ ) -> Result<(Option<TxSignatures>, Option<Transaction>), String> {
+ if self.has_received_tx_signatures {
+ return Err("Already received a tx_signatures message".to_string());
+ }
if self.remote_inputs_count() != tx_signatures.witnesses.len() {
- return Err(());
+ return Err("Witness count did not match contributed input count".to_string());
}
self.unsigned_tx.add_remote_witnesses(tx_signatures.witnesses.clone());
+ self.has_received_tx_signatures = true;
let holder_tx_signatures = if !self.holder_sends_tx_signatures_first {
self.holder_tx_signatures.clone()
@@ -447,20 +447,34 @@ impl InteractiveTxSigningSession {
/// unsigned transaction.
pub fn provide_holder_witnesses(
&mut self, channel_id: ChannelId, witnesses: Vec<Witness>,
- ) -> Result<(), ()> {
- if self.local_inputs_count() != witnesses.len() {
- return Err(());
+ ) -> Result<(Option<Transaction>, Option<TxSignatures>), String> {
+ let local_inputs_count = self.local_inputs_count();
+ if local_inputs_count != witnesses.len() {
+ return Err(format!(
+ "Provided witness count of {} does not match required count for {} inputs",
+ witnesses.len(),
+ local_inputs_count
+ ));
+ }
+ if self.holder_tx_signatures.is_some() {
+ return Err("Holder witnesses were already provided".to_string());
}
self.unsigned_tx.add_local_witnesses(witnesses.clone());
self.holder_tx_signatures = Some(TxSignatures {
channel_id,
+ witnesses,
tx_hash: self.unsigned_tx.compute_txid(),
- witnesses: witnesses.into_iter().collect(),
shared_input_signature: None,
});
- Ok(())
+ let funding_tx_opt = self.has_received_tx_signatures.then(|| self.finalize_funding_tx());
+ let holder_tx_signatures =
+ (self.holder_sends_tx_signatures_first || self.has_received_tx_signatures).then(|| {
+ debug_assert!(self.has_received_commitment_signed);
+ self.holder_tx_signatures.clone().expect("Holder tx_signatures were just provided")
+ });
+ Ok((funding_tx_opt, holder_tx_signatures))
}
pub fn remote_inputs_count(&self) -> usize {
@@ -507,6 +521,7 @@ impl_writeable_tlv_based!(InteractiveTxSigningSession, {
(3, holder_sends_tx_signatures_first, required),
(5, has_received_commitment_signed, required),
(7, holder_tx_signatures, required),
+ (9, has_received_tx_signatures, required),
});
#[derive(Debug)]
@@ -1090,6 +1105,7 @@ macro_rules! define_state_transitions {
holder_sends_tx_signatures_first: tx.holder_sends_tx_signatures_first,
unsigned_tx: tx,
has_received_commitment_signed: false,
+ has_received_tx_signatures: false,
holder_tx_signatures: None,
};
Ok(NegotiationComplete(signing_session))
Why this scored 34/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.