-
Notifications
You must be signed in to change notification settings - Fork 411
Introduce splice-compatible commitment update monitor variants #3855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Introduce splice-compatible commitment update monitor variants #3855
Conversation
This is a new `ChannelMonitorUpdateStep` variant intended to be used whenever a new funding transaction for the channel has been negotiated via the `InteractiveTxConstructor`. This commit primarily focuses on its use for splices, but future work will expand where needed to support RBFs (both for the initial dual funding transaction, and splice transactions). To draw a parallel to channel open, we generally want to have the commitment transactions negotiated for the funding transaction and committed to the respective `ChannelMonitor` before attempting to sign the funding transaction itself. This monitor update fulfills this need for a newly negotiated splice; it includes both the new holder and counterparty commitment transactions, and the new set of applicable `ChannelTransactionParameters`. Once the monitor update has been applied to the monitor and persisted, we allow the release of our `tx_signatures` for the splice transaction to wait for its confirmation.
This new variant is a backwards-incompatible successor to `LatestHolderCommitmentTXInfo` that is capable of handling holder commitment updates while a splice is pending. Since all holder commitment candidates share the same set of non-dust and dust HTLCs (though each non-dust HTLC can have differing output indices), we can share the same set of HTLC source data across all candidates.
This new variant is a backwards-incompatible successor to `LatestCounterpartyCommitmentTXInfo` that is capable of handling counterparty commitment updates while a splice is pending. Since all counterparty commitment candidates share the same set of non-dust and dust HTLCs (though each non-dust HTLC can have differing output indices), we can share the same set of HTLC source data across all candidates. Unfortunately, each `FundingScope` tracks its own set of `counterparty_claimable_outpoints`, which includes the HTLC source data (though only for the current and previous counterparty commitments), so we end up duplicating it (both in memory and on disk) temporarily.
👋 Thanks for assigning @TheBlueMatt as a reviewer! |
let htlcs_for_commitment = |commitment: &CommitmentTransaction| { | ||
let mut nondust_htlcs = commitment.nondust_htlcs().clone().into_iter(); | ||
let mut sources = htlc_data.nondust_htlc_sources.clone().into_iter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too happy with this, but trying to not duplicatively track the HTLC sources across FundingScope
s is a good bit of non-trivial work. At least they'll only exist in the current and previous counterparty commitments, but those are still per FundingScope
. The simplest thing we could do for now is Arc
them to reduce memory bandwidth (unfortunately on disk we'll still have duplicates), but I chose not to pursue that unless we're sure of it.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3855 +/- ##
==========================================
- Coverage 89.88% 89.71% -0.18%
==========================================
Files 160 163 +3
Lines 129654 131592 +1938
Branches 129654 131592 +1938
==========================================
+ Hits 116534 118052 +1518
- Misses 10425 10842 +417
- Partials 2695 2698 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks dude, just went through the first commit, will continue tomorrow. I have a big-picture question in commitment_signed_batch
take a look at that first.
) { | ||
let dust_htlcs: Vec<_> = if htlc_outputs.iter().any(|(_, s, _)| s.is_some()) { | ||
) -> Result<(), &'static str> { | ||
let htlc_data = if htlc_outputs.iter().any(|(_, s, _)| s.is_some()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we keep this to dust_htlcs
and build CommitmentHTLCData
just once right after this statement ?
return Err("Commitment transaction(s) mismatch"); | ||
} | ||
|
||
let mut current_funding_commitment = commitment_txs.remove(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried a few quick things to remove this O(n) remove
call here, didn't find anything. Are we cool with this ?
return Err("Commitment transaction(s) mismatch"); | ||
} | ||
|
||
let mut current_funding_commitment = commitment_txs.remove(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Also seems to me the code would read smoother if we called this holder_commitment_tx
self.prev_holder_htlc_data = Some(holder_htlc_data); | ||
fn update_holder_commitment_data( | ||
&mut self, mut commitment_txs: Vec<HolderCommitmentTransaction>, | ||
mut htlc_data: CommitmentHTLCData, claimed_htlcs: Vec<(SentHTLCId, PaymentPreimage)>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: similar style as holder_commitment_tx
, use holder_htlc_data
?
trusted_tx.built_transaction().transaction.tx_in(0).map(|input| input.previous_output).ok(); | ||
let expected_funding_outpoint_spent = | ||
pending_funding.channel_parameters.funding_outpoint.map(|op| op.into_bitcoin_outpoint()); | ||
if funding_outpoint_spent != expected_funding_outpoint_spent { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also check this for the current holder commitment tx ?
commitment_tx.clone(), htlc_outputs.clone(), &claimed_htlcs, | ||
nondust_htlc_sources.clone() | ||
) { | ||
log_error!(logger, "Failed updating latest holder commitment transaction info: {}", e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we set ret = Err(());
here and below ?
if htlc.transaction_output_index.is_some() { | ||
if htlc.offered { | ||
if let Some(source) = source_opt.take() { | ||
nondust_htlc_sources.push(source.clone()); | ||
} else { | ||
panic!("Missing outbound HTLC source"); | ||
} | ||
} | ||
} else { | ||
dust_htlcs.push((htlc, source_opt.take().cloned())); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we touch this, I think we can remove two indents here like this:
if htlc.transaction_output_index.is_some() && htlc.offered {
nondust_htlc_sources.push(source_opt.take().expect("Missing outbound HTLC source").clone());
} else if htlc.transaction_output_index.is_none() {
dust_htlcs.push((htlc, source_opt.take().cloned()));
}
let mut commitment_txs = Vec::with_capacity(self.pending_funding.len() + 1); | ||
let mut htlc_data = None; | ||
for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) { | ||
let funding_txid = funding.get_funding_txo().unwrap().txid; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Let's add a txid function here like we have in monitor ? I know it was here before, just a thought I've been having here.
} | ||
|
||
self.context.latest_monitor_update_id += 1; | ||
let mut monitor_update = ChannelMonitorUpdate { | ||
update_id: self.context.latest_monitor_update_id, | ||
updates, | ||
updates: vec![update], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far sounds like we don't need this to be a vector.
let msg = messages | ||
.get(&funding_txid) | ||
.ok_or_else(|| ChannelError::close(format!("Peer did not send a commitment_signed for pending splice transaction: {}", funding_txid)))?; | ||
let (commitment_tx, htlcs_included) = self.context.validate_commitment_signed( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might not be worth the pain let me know:
Can we avoid doing the work of populating and sorting this table for all the pending scopes in build_commitment_transaction
? We only need it for the current scope here.
(In validate_commitment_signed
, let's read the HTLCs from the commitment transaction instead of the HTLC-source table).
Also wondering if we can make the state and dust-vs-nondust HTLC sorting only once for all scopes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went through the second commit, some similar questions, some new ones.
@@ -3175,11 +3171,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { | |||
self.counterparty_hash_commitment_number.insert(htlc.payment_hash, commitment_number); | |||
} | |||
|
|||
log_trace!(logger, "Tracking new counterparty commitment transaction with txid {} at commitment number {} with {} HTLC outputs", txid, commitment_number, htlc_outputs.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You find this log not necessary ?
let expected_funding_outpoint_spent = | ||
pending_funding.channel_parameters.funding_outpoint.map(|op| op.into_bitcoin_outpoint()); | ||
if funding_outpoint_spent != expected_funding_outpoint_spent { | ||
return Err("Funding outpoint mismatch"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question here about also double checking the funding outpoint of the commitment transaction for confirmed / current the funding scope ?
}; | ||
|
||
let mut source = None; | ||
if htlc.offered { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like you explained in channel, should we be checking for accepted HTLCs here ?
let nondust_htlcs = core::iter::from_fn(move || { | ||
let htlc = if let Some(htlc) = nondust_htlcs.next() { | ||
htlc | ||
} else { | ||
debug_assert!(sources.next().is_none()); | ||
return None; | ||
}; | ||
|
||
let mut source = None; | ||
if htlc.offered { | ||
source = Some(Box::new(sources.next().expect( | ||
"Every offered non-dust HTLC should have a corresponding source" | ||
))); | ||
} | ||
|
||
Some((htlc, source)) | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some cleanups on this section:
debug_assert!(sources.len() <= nondust_htlcs.len());
let nondust_htlcs = core::iter::from_fn(move || {
let htlc = nondust_htlcs.next()?;
let source = (!htlc.offered).then(|| {
Box::new(sources.next().expect(
"Every offered non-dust HTLC should have a corresponding source"
))
});
Some((htlc, source))
});
}; | ||
|
||
let current_funding_commitment = commitment_txs.first().unwrap(); | ||
for (pending_funding, commitment_tx) in self.pending_funding.iter_mut().zip(commitment_txs.iter()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we off by one here? In the holder version we remove the first commitment transaction from the vector, then zip the vector together with the funding scopes.
return Err("Funding outpoint mismatch"); | ||
} | ||
|
||
pending_funding.prev_counterparty_commitment_txid = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question here about finishing all validation before mutating state.
@@ -3175,11 +3171,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { | |||
self.counterparty_hash_commitment_number.insert(htlc.payment_hash, commitment_number); | |||
} | |||
|
|||
log_trace!(logger, "Tracking new counterparty commitment transaction with txid {} at commitment number {} with {} HTLC outputs", txid, commitment_number, htlc_outputs.len()); | |||
self.funding.prev_counterparty_commitment_txid = self.funding.current_counterparty_commitment_txid.take(); | |||
self.funding.current_counterparty_commitment_txid = Some(txid); | |||
self.funding.counterparty_claimable_outpoints.insert(txid, htlc_outputs.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Came across this while reading the code: does not seem like we need the clone here ?
trusted_tx.built_transaction().transaction.tx_in(0).map(|input| input.previous_output).ok(); | ||
let expected_funding_outpoint_spent = | ||
pending_funding.channel_parameters.funding_outpoint.map(|op| op.into_bitcoin_outpoint()); | ||
if funding_outpoint_spent != expected_funding_outpoint_spent { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check here passes if both sides are None
; should never happen I agree, my paranoia speaking.
@@ -9379,32 +9379,54 @@ impl<SP: Deref> FundedChannel<SP> where | |||
} | |||
self.context.resend_order = RAACommitmentOrder::RevokeAndACKFirst; | |||
|
|||
let mut updates = Vec::with_capacity(self.pending_funding.len() + 1); | |||
for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) { | |||
let update = if self.pending_funding.is_empty() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future we'll still use the new variant even if we have no pending scopes right ?
let commitment_txs = core::iter::once(&self.funding) | ||
.chain(self.pending_funding.iter()) | ||
.map(|funding| { | ||
let (htlcs_ref, counterparty_commitment_tx) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question about only needing htlcs_ref a single time / some work getting duplicated across funding scopes.
Depends on #3822.