Skip to content

Support async signing of splice shared input#4579

Open
wpaulino wants to merge 1 commit intolightningdevkit:mainfrom
wpaulino:async-sign-shared-input
Open

Support async signing of splice shared input#4579
wpaulino wants to merge 1 commit intolightningdevkit:mainfrom
wpaulino:async-sign-shared-input

Conversation

@wpaulino
Copy link
Copy Markdown
Contributor

@wpaulino wpaulino commented Apr 29, 2026

While user signatures may be provided whenever ready at the user's discretion when handling a FundingTransactionReadyForSigning event, it does not cover the user's signature for the 2-of-2 multisig input in a splice. This signature is obtained via the EcdsaChannelSigner, which did not support providing it asynchronously.

Since the splice shared input signature is part of the tx_signatures message, we're not allowed to send the message until it's complete. This results in us needing to explicitly handle the signature exchange logic when the signer unblocks the shared input signature.

Fixes #4533.

@wpaulino wpaulino added this to the 0.3 milestone Apr 29, 2026
@wpaulino wpaulino requested a review from TheBlueMatt April 29, 2026 18:51
@wpaulino wpaulino self-assigned this Apr 29, 2026
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 29, 2026

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment on lines +13990 to +14001
if let Some((splice_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::Splice { .. }));
log_info!(
logger,
"Broadcasting signed splice transaction with txid {}",
splice_tx.compute_txid(),
);
self.tx_broadcaster.broadcast_transactions(&[(&splice_tx, tx_type)]);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: The variable name splice_tx and the debug_assert!(matches!(tx_type, TransactionType::Splice { .. })) assume this funding_tx is always a splice transaction. While this assertion is correct for all currently reachable paths (V2 initial funding cannot reach on_tx_signatures_exchange from signer_maybe_unblocked because the counterparty can't send tx_signatures before receiving our commitment_signed), the FundingTxSigned struct is generic enough to carry either type. If V2 dual-funding support evolves and this path becomes reachable for initial funding, the assert would fire and emit_channel_pending_event! would be missing (unlike the internal_tx_signatures handler which calls broadcast_interactive_funding).

Consider either renaming splice_txfunding_tx and handling both cases, or at minimum adding a comment explaining why this is splice-only.

Comment on lines +9957 to +9992
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
};
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: the return value of provide_holder_shared_input_signature (which includes (Option<TxSignatures>, Option<Transaction>)) is discarded at line 9974 and then re-fetched via signing_session.holder_tx_signatures() and signing_session.signed_tx() at lines 9986-9987. Each call to signed_tx() internally calls holder_tx_signatures() again, resulting in 3 total calls to holder_tx_signatures() (which clones and rebuilds each time). Not a correctness issue, but you could reuse the values from provide_holder_shared_input_signature to avoid redundant cloning.

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()?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note: signed_tx() previously accessed self.holder_tx_signatures (the field) directly, but now calls self.holder_tx_signatures() (the method). The method adds two filters:

  1. Shared input signature must be present (new in this PR - correct for async signing)
  2. Timing condition: (has_received_commitment_signed && holder_sends_tx_signatures_first) || has_received_tx_signatures()

The timing filter is effectively a no-op here because counterparty_tx_signatures.clone()? on the next line already returns None if counterparty hasn't sent theirs. But it changes signed_tx() from being purely about "are all signatures available" to also encoding protocol ordering constraints.

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented Apr 29, 2026

Review Summary

New issue found

  • lightning/src/ln/channelmanager.rs:14010Compilation error: Event::SplicePending does not exist; it was renamed to Event::SpliceNegotiated in merged PR Include failure context in splice events #4514. The signer_unblocked handler emits the wrong (nonexistent) event variant.
  • lightning/src/ln/async_signer_tests.rs:1819-1820 — Same compilation error: test uses Event::SplicePending instead of Event::SpliceNegotiated.

Prior comments status (still applicable)

  • channelmanager.rs:14001 — splice-only assumption in signer_unblocked tx broadcast
  • channel.rs:10050 — redundant cloning via discarded provide_holder_shared_input_signature return value
  • interactivetxs.rs:747signed_tx() behavioral change adding timing filter
  • channelmanager.rs:13944 — missing counterparty_initial_commitment_signed_result assert (note: was added at lines 13975-13981 after the prior comment, so this is resolved)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 85.20408% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.13%. Comparing base (42e198c) to head (fc1921c).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 79.48% 13 Missing and 3 partials ⚠️
lightning/src/ln/interactivetxs.rs 84.61% 6 Missing and 2 partials ⚠️
lightning/src/ln/channelmanager.rs 93.22% 2 Missing and 2 partials ⚠️
lightning/src/util/test_channel_signer.rs 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4579      +/-   ##
==========================================
- Coverage   87.15%   86.13%   -1.03%     
==========================================
  Files         161      157       -4     
  Lines      109251   108916     -335     
  Branches   109251   108916     -335     
==========================================
- Hits        95215    93810    -1405     
- Misses      11560    12490     +930     
- Partials     2476     2616     +140     
Flag Coverage Δ
fuzzing-fake-hashes ?
fuzzing-real-hashes ?
tests 86.13% <85.20%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning/src/ln/channel.rs Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TBH it seems like a weird API to pass shared_input_signature None when it was a splice. At a minimum it needs to be documented but it kinda feels like it'd be nicer to separately call provide_holder_shared_input_signature, dunno how crazy the refactor would be though. Same with the call to on_tx_signatures_exchange below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let me know if what I pushed is what you had in mind. Not sure what you meant by the on_tx_signatures_exchange comment though.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yea, this is much better.

@@ -612,9 +613,21 @@ impl InteractiveTxSigningSession {
self.holder_tx_signatures.is_some()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't most callers of this now be checking if we have the shared input as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I already accounted for this. All callers of has_holder_tx_signatures now only care about whether the user approved the transaction by signing, not necessarily if it's fully signed (with the shared input signature).

@wpaulino wpaulino force-pushed the async-sign-shared-input branch from 9df5f83 to fc1921c Compare May 6, 2026 23:42
Comment thread lightning/src/ln/channelmanager.rs
@wpaulino wpaulino force-pushed the async-sign-shared-input branch from fc1921c to 436c5fb Compare May 7, 2026 16:48
@wpaulino wpaulino requested review from TheBlueMatt and jkczyz May 7, 2026 16:48
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment on lines +729 to +734
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());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should these never happen? i.e., We'll only call provide_holder_shared_input_signature after the user has called funding_transaction_signed and only for a splice.

Any reason why we don't allow providing the shared signature independent of the other witnesses? Or is it simpler to just query the signer once we have the other witnesses?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They should never happen, we could debug_assert instead if you'd like. No particular reason to not do it before funding_transaction_signed, but why bother signing something when there's a possibility it gets canceled instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, right makes sense in case it is canceled. No strong opinion on the assertion.

Comment thread lightning/src/ln/interactivetxs.rs Outdated
.map(|tx_signatures| {
self.shared_input().is_some() && tx_signatures.shared_input_signature.is_none()
})
.unwrap_or(false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see how this is called, but it seems odd that we are only considered awaiting them once the user has provided their own witnesses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@wpaulino wpaulino force-pushed the async-sign-shared-input branch from 436c5fb to 6cc0b8c Compare May 7, 2026 22:16
While user signatures may be provided whenever ready at the user's
discretion when handling a `FundingTransactionReadyForSigning` event, it
does not cover the user's signature for the 2-of-2 multisig input in a
splice. This signature is obtained via the `EcdsaChannelSigner`, which
did not support providing it asynchronously.

Since the splice shared input signature is part of the `tx_signatures`
message, we're not allowed to send the message until it's complete. This
results in us needing to explicitly handle the signature exchange logic
when the signer unblocks the shared input signature.
@wpaulino wpaulino force-pushed the async-sign-shared-input branch from 6cc0b8c to 25cd9e0 Compare May 7, 2026 22:18
Comment thread lightning/src/ln/channelmanager.rs
Comment thread lightning/src/ln/async_signer_tests.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sign_splice_shared_input doesn't support async

5 participants