Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Refactor `ConstructedTransaction` to contain `Transaction` by jkczyz · Pull Request #4097 · lightningdevkit/rust-lightning · GitHub
Skip to content

Refactor ConstructedTransaction to contain Transaction - #4097

Merged
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor
Sep 24, 2025
Merged

Refactor ConstructedTransaction to contain Transaction#4097
wpaulino merged 10 commits into
lightningdevkit:mainfrom
jkczyz:2025-09-interactive-tx-refactor

Conversation

@jkczyz

Copy link
Copy Markdown
Contributor

ConstructedTransaction tracks the result of the InteractiveTxConstruction and is used during an InteractiveTxSigningSession. This PR refactors it such that it holds an actual Transaction rather than the contributed parts, though some metadata in the form of NegotiatedTxInput and NegotiatedTxOutput need to be maintained.

This allows us to re-use the weight calculation provided by Transaction. It also facilitates reconstructing contributed inputs and outputs to produce a SpliceFailed event in #4077.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @wpaulino 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.

@codecov

codecovBot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.20183% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.71%. Comparing base (3564646) to head (86ed012).
⚠️ Report is 109 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/interactivetxs.rs93.33%11 Missing and 3 partials ⚠️
lightning/src/ln/channel.rs25.00%3 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4097 +/- ##
==========================================
+ Coverage 87.81% 88.71% +0.89% 
==========================================
Files 176 177 +1 Lines 131770 133237 +1467 Branches 131770 133237 +1467 ==========================================
+ Hits 115719 118196 +2477 + Misses 13416 12326 -1090 - Partials 2635 2715 +80 
FlagCoverage Δ
fuzzing21.74% <0.54%> (+0.14%)⬆️
tests88.55% <92.20%> (+0.89%)⬆️

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.

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Wondering if we should have InteractiveTxSigningSession store both local and remote signatures until finalize_funding_tx is called. That way, ConstructedTransaction would always contain an unsigned transaction instead of one that may be partially signed.

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.

SGTM, we already store the local ones separately

InteractiveTxConstructor::new converts inputs given in channel.rs to
InputOwned. Instead of converting FundingTxInput in channel.rs to the
type needed by that constructor, pass in FundingTxInput and have it
converted directly to InputOwned there. This saves one conversion and
Vec allocation. It also lets us use the satisfaction_weight in an
upcoming refactor.
Instead of estimating the input weight, track the satisfaction_weight in
InteractiveTxInput. This then can be used compute the transaction weight
without storing individual weights in NegotiatedTxInput in an upcoming
commit.
The weight is no longer needed once a ConstructedTransaction is created,
so it doesn't need to be persisted.
Instead of defining a custom function for calculating a transaction's
weight, have ConstructedTransaction hold a Transaction so that its
weight method can be re-used. As a result NegotiatedTxInput no longer
needs to store the transaction inputs.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from f96f304 to 9eeaf52CompareSeptember 19, 2025 20:05
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Pushed rustfmt fixes.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! 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 threadlightning/src/ln/funding.rs
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment on lines 350 to 365
fn add_local_witnesses(&mut self, witnesses: Vec<Witness>) {
self.inputs
self.tx
.input
.iter_mut()
.zip(self.inputs.iter())
.enumerate()
.filter(|(_, input)| input.is_local(self.holder_is_initiator))
.filter(|(_, (_, input))| input.is_local(self.holder_is_initiator))
.filter(|(index, _)| {
self.shared_input_index
.map(|shared_index| *index != shared_index as usize)
.unwrap_or(true)
})
.map(|(_, input)| &mut input.txin)
.map(|(_, (txin, _))| txin)
.zip(witnesses)
.for_each(|(input, witness)| input.witness = witness);
}

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.

SGTM, we already store the local ones separately

Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
Comment threadlightning/src/ln/interactivetxs.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

Now that NegotiatedTxInput no longer holds a TxIn, rename it
accordingly.
Now that ConstructedTransaction holds the actual Transaction, there's no
need to keep track of and persist the outputs separately. However, the
serial IDs are still needed to later reconstruct which outputs were
contributed.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 9eeaf52 to 7958dd9CompareSeptember 22, 2025 23:37
@jkczyzjkczyz added this to the 0.2 milestone Sep 23, 2025
@jkczyzjkczyz self-assigned this Sep 23, 2025
The output values are never used so there's no need to persist them.
If needed, they can be re-computed though the shared output's local
and remote amounts would be lost.
The local and remote input values are used to determine which node sends
tx_signatures first. Instead of persisting these values, compute them
only when needed from the input metadata. The spec states that the
entire shared input value is to be included for the node sending the
corresponding tx_add_input, so it isn't necessary to know the local and
remote balances which the metadata does not contain.
InteractiveTxSigningSession currently persists holder witnesses
directly, but persists counterparty witnesses as part of its unsigned
ConstructedTransaction. This makes the ConstructedTransaction actually
partially signed even though it is held in a field named unsigned_tx.
Instead, persists the counterparty witnesses alongside the holder
witnesses directly in InteractiveTxSigningSession, leaving the
transaction it holds unsigned.
@jkczyz
jkczyzforce-pushed the 2025-09-interactive-tx-refactor branch from 7958dd9 to 86ed012CompareSeptember 23, 2025 16:53

@TheBlueMattTheBlueMatt left a comment

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.

Code changes look fine to me. I'm not deep enough in the interactive negotiation world to say whether its all a good idea, but seems fine to me.

.as_ref()
.map(|shared_input| SharedInputSignature {
holder_signature_first: shared_input.holder_sig_first,
counterparty_signature: None,

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 probably rename SharedInputSignature since it doesn't contain a signature anymore

if let Some(shared_input_index) = self.shared_input_index {
let holder_shared_input_sig =
holder_tx_signatures.shared_input_signature.or_else(|| {
debug_assert!(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 think it'd be better to debug_assert_eq!(shared_input_index.is_some(), holder_tx_signatures.shared_input_signature.is_some()) to catch the other possible cases as well

@wpaulino
wpaulino merged commit 5a84daf into lightningdevkit:mainSep 24, 2025
40 of 45 checks passed
Sign up for freeto 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.

4 participants

@jkczyz@ldk-reviews-bot@TheBlueMatt@wpaulino