-
Notifications
You must be signed in to change notification settings - Fork 411
Don't include below-dust inbound HTLCs in commit tx fee calculation #762
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
Don't include below-dust inbound HTLCs in commit tx fee calculation #762
Conversation
Codecov Report
@@ Coverage Diff @@
## main #762 +/- ##
==========================================
+ Coverage 90.76% 90.80% +0.03%
==========================================
Files 44 44
Lines 24282 24466 +184
==========================================
+ Hits 22040 22216 +176
- Misses 2242 2250 +8
Continue to review full report at Codecov.
|
This looks good to me. I'll let the fuzzer spin on this + 756 for a bit and see what it thinks. |
Fuzzers didn't find anything after 15 CPU-days, so probably at least fixes that failing case. Obviously can't rely on fuzzers so should add an explicit test (yay |
fa57462
to
84149fd
Compare
Still needs a bit more testing but I pushed up one test and an additional fix (the fix being: when calculating the commit tx fee, don't include the HTLC currently being added if it's below dust). |
84149fd
to
1faf0da
Compare
lightning/src/ln/channel.rs
Outdated
let mut their_acked_htlcs = self.pending_inbound_htlcs.len(); | ||
let mut addl_htlcs = 1; | ||
if fee_spike_buffer_htlc.is_some() { addl_htlcs += 1; } | ||
if new_htlc_amount / 1000 <= self.counterparty_dust_limit_satoshis { |
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 the counterparty/holder dust limit references in this PR be swapped? If its easy, would be nice to get a test which would catch such an inversion.
1faf0da
to
15a92be
Compare
Found some more missing pieces so I'm restructuring to reuse some of the logic in |
If you're refactoring bits of |
15a92be
to
f165fca
Compare
Is there a way we can do a more thorough test of this? eg in |
That makes sense, currently trying to implement this! |
f165fca
to
068543e
Compare
Ok, I believe I've approximately added this testing. |
We should probably enable the new fee caching stuff in fuzztarget, too. |
Note that the new tests, with just the |
ba5ea8c
to
5754f42
Compare
Ah, I take that back, the |
3b02a56
to
076137d
Compare
OK I believe I've fixed this and |
It looks like |
I think my current approach in 076137d causes too many bespoke exceptions to be necessary. Gonna try to rework it. |
Right, I was kinda afraid of that. I'm mostly OK with a ton of exceptions in #[cfg(test)] blocks, but if you can find a simpler way that would be cool too. |
076137d
to
e6a3fe3
Compare
Lmk what you think of the new approach. |
Right, much better, thanks! I am a little worried about the "info.num_htlcs == counterparty_commitment_tx.1" checks neutering some of the value - isn't part of the reason for the checks to make sure we have the same number of HTLCs as we thought we would? Is there a way to take a bit of both approaches and maybe just drop that check and then reset the states on RAA or commitment_signed messages? |
e6a3fe3
to
3b6000d
Compare
Adding those specific checks^ still had test failures. I agree the |
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.
Right. Looks good to me! I added one note about some manual mutation testing that didn't get caught, if its easy would be nice, but if its a pain I'm ok without.
137346c
to
ebf5a06
Compare
&& info.next_holder_htlc_id == self.next_holder_htlc_id | ||
&& info.next_counterparty_htlc_id == self.next_counterparty_htlc_id | ||
&& info.feerate == self.feerate_per_kw { | ||
assert_eq!(total_fee, info.fee / 1000); |
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.
Is there any way to write a test that checks that these agree instead of adding to the production code (albeit, only conditionally for tests). Generally, writing a test would be preferable if it is possible.
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 think the point of this block is more to check it in fuzz/functional tests than one-off. Would be great to get the test code our of channel.rs
, I agree, but not sure how to do that in the fuzzer/functional tests.
ebf5a06
to
d6860ec
Compare
2e4297b
to
2cc8aa7
Compare
Also remove part of the holding cell channel reserve test that's newly failing but a bit of a redundant test anyway.
2cc8aa7
to
1079753
Compare
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.
Post-merge Code Review ACK 1079753
Overall, a call for better comments given how much off-chain computation is tricky. Accounting a HTLC for fee computation is function of its state, and even if we follow the most sound reasoning IMO, I wouldn't be surprised that other implementation have weird recipes ("What if they have another definition of AwaitingRemoteRevokeToRemove
?"), thus leading to unexpected closure in case of edge-cases. Good documentation would help track down that kind of cases if they show up.
I think I've also found few test coverage holes.
assert!(self.is_outbound()); | ||
|
||
let mut their_acked_htlcs = self.pending_inbound_htlcs.len(); | ||
let real_dust_limit_success_sat = (self.feerate_per_kw as u64 * HTLC_SUCCESS_TX_WEIGHT / 1000) + self.holder_dust_limit_satoshis; |
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 think it would be great to document what does real dust_limit means, it's quite buried in BOLT3. I even think the spec should scope the HTLC output fee contribution to the commitment one, but it doesn't seem to be recommended anywhere...
OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, | ||
// We don't include AwaitingRemoteRevokeToRemove HTLCs because our next commitment | ||
// transaction won't be generated until they send us their next RAA, which will mean | ||
// dropping any HTLCs in this state. |
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 think this is correct, sequence where you might have HTLC in AwaitingRemoteRevokeToRemote
are either us sending update_fail/success_htlc
, commitment_signed
and yet trying to send one more HTLC or receive an update from remote.
Note, "dropping any HTLCs in this state" doesn't mean this HTLC won't materialize onchain. Receiving counterparty RAA doesn't guarantee then to receive a commitment_signed
with such HTLC effectively removed for holder commitment tx.
// Get the commitment tx fee for the remote's next commitment transaction based on the number of | ||
// pending HTLCs that are on track to be in their next commitment tx, plus an additional HTLC if | ||
// `fee_spike_buffer_htlc` is Some, plus a new HTLC given by `new_htlc_amount`. Dust HTLCs are | ||
// excluded. |
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.
May we prefix mention of dust with "economically" or something like this to mark the difference with Core' s notion of IsDust
to evaluate a transaction standardness ?
// +1 for this HTLC. | ||
self.next_remote_commit_tx_fee_msat(1) | ||
let htlc_candidate = HTLCCandidate::new(msg.amount_msat, HTLCInitiator::RemoteOffered); | ||
self.next_remote_commit_tx_fee_msat(htlc_candidate, None) // Don't include the extra fee spike buffer HTLC in calculations |
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.
No test coverage, mutating here
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index d3a372e4..b7f5d6a2 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -1969,7 +1969,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.is_outbound() { 0 } else {
let htlc_candidate = HTLCCandidate::new(msg.amount_msat, HTLCInitiator::RemoteOffered);
- self.next_remote_commit_tx_fee_msat(htlc_candidate, None) // Don't include the extra fee spike buffer HTLC in calculations
+ self.next_remote_commit_tx_fee_msat(htlc_candidate, Some(())) // Don't include the extra fee spike buffer HTLC in calculations
};
if pending_remote_value_msat - msg.amount_msat < remote_commit_tx_fee_msat {
return Err(ChannelError::Close("Remote HTLC add would not leave enough to pay for fees".to_owned()));
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1, | ||
OutboundHTLCState::Committed => included_htlcs += 1, | ||
OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, | ||
OutboundHTLCState::LocalAnnounced { .. } => included_htlcs += 1, |
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.
No test coverage, mutating here
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index d3a372e4..6751b973 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -1871,7 +1871,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
match htlc.state {
OutboundHTLCState::Committed => included_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1,
- OutboundHTLCState::LocalAnnounced { .. } => included_htlcs += 1,
+ OutboundHTLCState::LocalAnnounced { .. } => {}, //included_htlcs += 1,
_ => {},
}
}
for ref htlc in self.pending_outbound_htlcs.iter() { | ||
if htlc.amount_msat / 1000 <= self.counterparty_dust_limit_satoshis { | ||
if htlc.amount_msat / 1000 <= real_dust_limit_success_sat { |
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 think it would be great to comment this subtlety around which dust limit apply in function of which viewpoint the fee is computed for.
"When computing the fee as paid by counterparty, if they have to claim onchain HTLC by broadcasting their commitment, they will have to spend inbound through a HTLC-timeout, outbound through a HTLC-Success.
When computing the fee as paid by holder, if we have to claim onchain HTLC by broadcasting our commitment, we will have to spend inbound through a HTLC-success, outbound through a HTLC-timeout"
Well, that's an attempt, but we should make it clear that even if our channel variables represent the holder direction-side (pending_inbound_htlcs
/pending_outbound_htlcs
) the actual fee cost is function from which commitment they will be claim.
Thanks for the post-merge review @ariard, will attempt to address soon^tm |
Closes #757
Does this fix look right? Seems like an oops from the original channel reserve PR. Let me know and I'll write tests.