Skip to content

Implement update_fee handling #161 #187

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

Closed
wants to merge 14 commits into from

Conversation

yuntai
Copy link
Contributor

@yuntai yuntai commented Sep 18, 2018

I'm not confident I know what I am doing with this PR. Pushed to get any feedback first, please. State stuff is complex!

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

At a high level looks really good, but I think there's some race conditions here. The commitment_signed/revoke_and_ack dances are all really subtle. Generally, we should upate Channel::feerate_per_kw upon revoke_and_ack coming from the peer that did not generate the update_fee. In other words, if the channel is inbound, we should update self.feerate_per_kw when we receive a commitment_signed and respond with a revoke_and_ack (which you do), and if the channel is outbound, we should update self.feerate_per_kw when we receive an inbound revoke_and_ack.

@@ -1800,6 +1818,20 @@ impl Channel {
}
self.value_to_self_msat = (self.value_to_self_msat as i64 + value_to_self_msat_diff) as u64;

if let Some(feerate) = self.pending_update_fee.take() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you need a self.channel_outbound check here just like you have in commitment_signed (but, obviously, opposite). Otherwise you might have:

Us                                    remote
                                 <-   update_fee
commitment_signed (incl new fee) ->

which would be invalid because the remote side needs to send us their commitment_signed before we use the new feerate.

Copy link
Contributor Author

@yuntai yuntai Sep 19, 2018

Choose a reason for hiding this comment

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

With current logic, for an inbound channel, self.pending_update_fee.is_none()==true inside revoke_and_ack(). I'm worried that the logic may be correct for passing tests but only with our implementation, but the implemented logic is in sync with your general description given in your first comment.

pub fn send_update_fee_and_commit(&mut self, feerate_per_kw: u64) -> Result<Option<(msgs::UpdateFee, msgs::CommitmentSigned, ChannelMonitor)>, HandleError> {
match self.send_update_fee(feerate_per_kw)? {
Some(update_fee) => {
let (commitment_signed, monitor_update) = self.send_commitment_no_status_check()?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I don't think this is quite right:
When we call send_update_fee_and_commit we'll create the update_fee message, send it to our peer, then create a commitment_signed message with the new fee (that is all correct). However, when we create the commitment_signed message we'll set self.feerate_per_kw, which means we'll be incorrect in some race conditions.

What should happen is this:

Us                                    remote
update_fee                       ->
commitment_signed (incl new fee) ->
                                 <- update_add_htlc
                                 <- commitment_signed (not incl new fee)

But we'll expect their commitment_signed to use the new feerate. This is incorrect and we should instead only expect that after they send a new revoke_and_ack.

You may want to add a test which tests the above message exchanges.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure how to generate the condition above with our code. The handler of commitment_signed generates revoke_and_ack and commitment_signed, and put the channel in AwaitingRemoteRevoke state. So the second commitment signed will use new fee after 'remote' having received revoke_and_ack from 'Us.'

If you meant update_add_htlc/commitment_signed arriving to 'Us' when the first commitment_signed in transit, then it's covered, I think, because 'Us' does not update self.feerate until it gets revoke_and_ack from 'remote.'

Copy link
Collaborator

Choose a reason for hiding this comment

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

You should be able to create two channelmanagers - one call update_fee and get the three messages out of it, on the other send_payment. Now forward the messages to both ends but only after calling both and ensure they all behave correctly.

@@ -591,6 +591,9 @@ impl<Descriptor: SocketDescriptor> PeerManager<Descriptor> {
for resp in resps.update_fail_htlcs {
encode_and_send_msg!(resp, 131);
}
if let Some(resp) = resps.update_fee {
encode_and_send_msg!(resp, 131);
Copy link
Collaborator

Choose a reason for hiding this comment

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

update_fee is type 134, not 131.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The 131 should be changed to a 134?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This still isnt changed?

@@ -284,6 +284,9 @@ pub struct Channel {
pending_inbound_htlcs: Vec<InboundHTLCOutput>,
pending_outbound_htlcs: Vec<OutboundHTLCOutput>,
holding_cell_htlc_updates: Vec<HTLCUpdateAwaitingACK>,

pending_update_fee: Option<u64>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment above these describing at a high level what the conditions are for updating them?

@@ -1705,6 +1722,7 @@ impl Channel {
update_fulfill_htlcs,
update_fail_htlcs,
update_fail_malformed_htlcs: Vec::new(),
update_fee: None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to release the holding_cell_fee_update here, actually.

@@ -1705,6 +1730,17 @@ impl Channel {
update_fulfill_htlcs,
update_fail_htlcs,
update_fail_malformed_htlcs: Vec::new(),
update_fee: {
if let Some(feerate) = self.holding_cell_update_fee {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs to happen before we generate the commitment_signed message as our new commitment must use the newly-updated fee.


pub fn send_update_fee(&mut self, feerate_per_kw: u64) -> Result<Option<msgs::UpdateFee>, HandleError> {
if !self.channel_outbound {
return Err(HandleError{err: "Cannot send fee from inbound channel", action: None});
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can just panic.


}

pub fn send_update_fee(&mut self, feerate_per_kw: u64) -> Result<Option<msgs::UpdateFee>, HandleError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: dont make this pub unless its actually used somewhere in ChannelManager (and document the "cannot be called twice without a send_commitment in between" invariant).

@@ -1923,6 +1927,36 @@ impl ChannelManager {
}
res
}

pub fn update_fee(&self, channel_id: [u8;32], feerate_per_kw: u64) -> Result<(), HandleError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we return an APIError instead here?

match channel_state.by_id.get_mut(&channel_id) {
None => return Err(HandleError{err: "Failed to find corresponding channel", action: None}),
Some(chan) => {
if !chan.is_usable() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should also check if the channel is outbound.

@@ -591,6 +591,9 @@ impl<Descriptor: SocketDescriptor> PeerManager<Descriptor> {
for resp in resps.update_fail_htlcs {
encode_and_send_msg!(resp, 131);
}
if let Some(resp) = resps.update_fee {
encode_and_send_msg!(resp, 131);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The 131 should be changed to a 134?

@yuntai yuntai force-pushed the 201809-update-fee branch 2 times, most recently from 42b5b02 to f73e2cc Compare September 20, 2018 16:32
@yuntai
Copy link
Contributor Author

yuntai commented Sep 20, 2018

ahh.. one more test (update mixed w/ payment) need to be added before a review.

@yuntai
Copy link
Contributor Author

yuntai commented Sep 21, 2018

One more test added. seems better now.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

I spent some real quality time with this one, since some interaction is super subtle. Ended up writing a few new tests to make sure I got everything. You can find a squashed copy of this PR (with a few macros pulled out to simplify the tests) as well as fixes for most of my comments here, as well as a few new tests, one of which currently fails and needs some work to make pass at https://github.com/TheBlueMatt/rust-lightning/commits/2018-09-187-tweaks. I think if you add another boolean for tracking whether the current pending feerate update has been acked on inbound channels it should be easy to make pass.

@@ -1582,7 +1613,7 @@ impl Channel {

let mut htlcs_and_sigs = Vec::with_capacity(local_commitment_tx.1.len());
for (idx, ref htlc) in local_commitment_tx.1.iter().enumerate() {
let mut htlc_tx = self.build_htlc_transaction(&local_commitment_txid, htlc, true, &local_keys);
let mut htlc_tx = self.build_htlc_transaction(&local_commitment_txid, htlc, true, &local_keys, self.feerate_per_kw);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this use the local copy instead of the old version?

if !self.channel_outbound {
if let Some(fee_update) = self.pending_update_fee {
self.feerate_per_kw = fee_update;
if (self.channel_state & ChannelState::AwaitingRemoteRevoke as u32) == 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is right, but its super subtle. Can you add a comment like "We later use the presence of pending_update_fee to indicate we should generate a commitment_signed upon receipt of revoke_and_ack, so we can only set it to None if we're not awaiting a revoke".

@@ -1694,18 +1736,28 @@ impl Channel {
//fail it back the route, if its a temporary issue we can ignore it...
match err {
None => {
if update_add_htlcs.is_empty() && update_fulfill_htlcs.is_empty() && update_fail_htlcs.is_empty() {
if update_add_htlcs.is_empty() && update_fulfill_htlcs.is_empty() && update_fail_htlcs.is_empty() && self.holding_cell_update_fee.is_none() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this interacts with #189 (well, ok, the send_htlc call above does). If we have pending fee updates they may invalidate our sends and may make this commitment_signed we're about to generate invalid. I'm fine with leaving it for #189 or post-merge-of-both, but should at least file an issue for it once they get merged.

if self.channel_outbound {
self.feerate_per_kw = feerate;
} else {
assert_eq!(feerate, self.feerate_per_kw);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is wrong, if a peer sends us an update_fee then revoke_and_ack prior to a commitment_signed (which is allowed if, eg, they are waiting on something else to send a commitment_signed) then we'll immediately crash.

Knee-jerk reaction is that we'll need another flag or another pending_fee_update variable, but I think it'd actually be fine to just check if feerate == self.feerate_per_kw { require_commitment = true; } and then add a comment about how we left pending_update_fee set because we were in AwaitingRemoteRevoke and the equality indicates we need a commitment. This is only OK because they aren't allowed to send a duplicative commitment_signed with an update_fee that simply hasn't changed.

After fighting with some new tests for a while, I think we need a new boolean field to track this correctly.

return None;
}

debug_assert!(self.pending_update_fee.is_none());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment describing why, since it's not quite clear why this is OK otherwise - if we just sent an update_fee then we should be in AwaitingRemoteRevoke as we should have been committed prior to any other updates, otherwise the revoke_and_ack should have wiped our pending_update_fee.

/// Begin Update fee process. Allowed only on an outbound channel.
/// If successful, will generate a UpdateHTLCs event, so you should probably poll
/// PeerManager::process_events afterwards.
pub fn update_fee(&self, channel_id: [u8;32], feerate_per_kw: u64) -> Result<(), APIError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make this priv for now for testing? I think the eventual API is gonna look something more like ChannelManager::notify_fees_changed_materially() and that will update all our channels and check if inbound channels have sane fees.

Or just mark it doc(hidden) and note that it'll change eventually.

@@ -591,6 +591,9 @@ impl<Descriptor: SocketDescriptor> PeerManager<Descriptor> {
for resp in resps.update_fail_htlcs {
encode_and_send_msg!(resp, 131);
}
if let Some(resp) = resps.update_fee {
encode_and_send_msg!(resp, 131);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This still isnt changed?

if let &Some(ref msg) = update_fee {
peer.pending_outbound_buffer.push_back(peer.channel_encryptor.encrypt_message(&encode_msg!(msg, 134)));
}
debug_assert!(update_fee.is_none());
Copy link
Collaborator

Choose a reason for hiding this comment

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

I presume this was just here for testing and should be removed (given ChannelManager::update_fee generates such an event)?

// This should never actually happen and indicates we got some Errs back
// from update_fulfill_htlc/update_fail_htlc, but we handle it anyway in
// case there is some strange way to hit duplicate HTLC removes.
return Ok(None);
}
let update_fee = if let Some(feerate) = self.holding_cell_update_fee {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is never actually hit because of the if self.holding_cell_htlc_updates.len() != 0 { check at the top of the function.

check_added_monitors_and_clear!(nodes[0]);

let resp_option = nodes[1].node.handle_revoke_and_ack(&nodes[0].node.get_our_node_id(), &revoke_msg_0).unwrap();
let commitment_signed = resp_option.unwrap().commitment_signed;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does nodes[1] generate a new commitment_signed here? AFAICT the contents are identical.

@yuntai
Copy link
Contributor Author

yuntai commented Sep 29, 2018

I applied the fixes in your local branch and made some tweaks for the inbound channel. Somehow this fix passes all the tests without additional variable but can't explain why!

@TheBlueMatt
Copy link
Collaborator

Ah, yea, I think your changes are right, see new comments in #200. I'll take that once travis passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants