Skip to content

Test for BOLT 2 requirements for update_add_htlc message #291

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

Conversation

philipr-za
Copy link
Contributor

Related to issue #129 and #240

This PR contributes a test to explicitly test each requirement for the sender and receiver for the update_add_htlc message as defined in the BOLT 2 spec.

Some notes:
I found that it did not appear that the following requirement was validated in the send_htlc method:

- MUST NOT offer amount_msat it cannot pay for in the remote commitment transaction at the current feerate_per_kw (see "Updating Fees") while maintaining its channel reserve.

but because the Fee aspect of the BOLT specs is being updated I left it as a TODO.

I also did not specifically test the following requirement:

- for channels with chain_hash identifying the Bitcoin blockchain:
    * MUST set the four most significant bytes of amount_msat to 0.

I could not find a sensible way to get around the many layers of checks that prevent amount_msat from being set to close to a value that violates this requirement.

Finally, I found that the following requirement on the Sender was not explicitly tested in the various methods involved in handling an update_add_htlc message:

Sender MUST set cltv_expiry less than 500000000.

It exists as a TODO in the send_htlc() but it is enforced when constructing a route.

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.

Awesome! Thanks! Would like a few cleanups but looks good.

}, onion_packet);

if let Err(ChannelError::Ignore(_)) = err {
assert!(true);
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 check the error message is the expected one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in all the tests

let (onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads(&route, cur_height).unwrap();
let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, &our_payment_hash);

let err = nodes[0].node.channel_state.lock().unwrap().by_id.get_mut(&chan.2).unwrap().send_htlc(0, our_payment_hash, TEST_FINAL_CLTV, HTLCSource::OutboundRoute {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why call through to the channel directly? Can't you test the same using nodes[0].node.send_payment and check the error message without stepping over a layer? (also again further down).

Copy link
Contributor Author

@philipr-za philipr-za Jan 16, 2019

Choose a reason for hiding this comment

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

The reason is that from what I can see when using send_payment(...) the payment parameters are all set up when you call get_route(...) and most of these test cases cause various failures when trying to construct the route. I was trying to test all the validation that is done when adding the HTLC to be sent rather than constructing the route so to get into the middle of that process I constructed a valid route and then implemented the essential stuff that is happening inside nodes[0].node.send_payment(...) in the test and changed the payment parameters for the test cases.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I think we're out of sync on which send_payment. I dont mean the functional_tests::send_payment function that steps through everything, I meant the ChannelManager::send_payment, which calls Channel::send_htlc pretty directly and takes the Route as a parameter.

Copy link
Contributor Author

@philipr-za philipr-za Jan 18, 2019

Choose a reason for hiding this comment

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

Ok, I figured out how to effect these tests without skipping that layer. I was originally trying to check for the correct error types which was tricky because the various error handling macro's changed the types into more generic versions as they were handled but by just looking for the error message strings it is possible to do it as you suggest.

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.

Awesome! I have a number of comments regarding making sure the tests don't break when we change minor implementation details and making sure we get the most out of the tests, but this is great work.

@@ -382,7 +394,7 @@ macro_rules! secp_check {

impl Channel {
// Convert constants + channel value to limits:
fn get_our_max_htlc_value_in_flight_msat(channel_value_satoshis: u64) -> u64 {
pub fn get_our_max_htlc_value_in_flight_msat(channel_value_satoshis: u64) -> u64 {
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 no longer necessary?

Copy link
Contributor Author

@philipr-za philipr-za Jan 21, 2019

Choose a reason for hiding this comment

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

changed back to be not private, I do need to query this value but do it via get_value_stat(...) now.

let max_accepted_htlcs = nodes[1].node.channel_state.lock().unwrap().by_id.get(&chan.2).unwrap().their_max_accepted_htlcs as u64;

//Confirm the first HTLC ID is zero
assert_eq!(nodes[0].node.channel_state.lock().unwrap().by_id.get(&chan.2).unwrap().next_local_htlc_id, 0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just check this based on the field in the update_add_htlc message? Then you dont have to expose more internal state (and check that the internal state is converted to messages correctly).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, took out making the internal state public for the test.

commitment_signed_dance!(nodes[1], nodes[0], payment_event.commitment_msg, false);

expect_pending_htlcs_forwardable!(nodes[1]);
let _ = nodes[1].node.get_and_clear_pending_events();
Copy link
Collaborator

Choose a reason for hiding this comment

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

expect_payment_received!().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, had to pull the definition of the macro out of the test it was in to make it available to the other tests.

let mut nodes = create_network(2);
let _chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 0);

let route = nodes[0].router.get_route(&nodes[1].node.get_our_node_id(), None, &[], 100000000, TEST_FINAL_CLTV).unwrap();
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 test the boundary? ie test that we will send exactly max_htlc_value_in_flight and then test that sending on more fails? This also means if we change the order of checks to check HTLCs dont add total_value - reserve value this test wont spuriously break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

let channel = chan_lock.by_id.get(&chan.2).unwrap();
htlc_minimum_msat = channel.get_our_htlc_minimum_msat();
}
let route = nodes[0].router.get_route(&nodes[1].node.get_our_node_id(), None, &[], htlc_minimum_msat+1, TEST_FINAL_CLTV).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldnt need the +1 here.

Copy link
Contributor Author

@philipr-za philipr-za Jan 21, 2019

Choose a reason for hiding this comment

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

If you don't the get_route(...) call fails saying it cannot find the route. You can indeed go in and change the message manually after constructing the route to have 1 less and it works but the route creation process seems to need that +1.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ahh, I'll drop it in #203, then!

check_added_monitors!(nodes[0], 1);
let mut updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id());

updates.update_add_htlcs[0].amount_msat = 4000001;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where did this come from/wont this break if/when we change our reserve values?

Copy link
Contributor Author

@philipr-za philipr-za Jan 21, 2019

Choose a reason for hiding this comment

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

Updated to be based on the queried channel_reserve value.

msg.htlc_id = i as u64;
nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &msg).unwrap();
}
msg.htlc_id = (super::channel::OUR_MAX_HTLCS + 1) as u64;
Copy link
Collaborator

Choose a reason for hiding this comment

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

The +1 is extraneous, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true, fixed

let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 1000000);
let route = nodes[0].router.get_route(&nodes[1].node.get_our_node_id(), None, &[], 1000000, TEST_FINAL_CLTV).unwrap();
let (_, our_payment_hash) = get_payment_preimage_hash!(nodes[0]);
nodes[0].node.send_payment(route, our_payment_hash).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: bad indentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

{
assert_eq!(nodes[1].node.channel_state.lock().unwrap().by_id.len(), 0);
}
//Clear unhandled msg events.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm really not a fan of doing this, generally. If we bothered to write out a test for a given behavior, we should check that the user-facing result of it is consistent with what we expect. If there are a bunch of things that happen, just add a macro that checks all of them then you can reuse the code.

Copy link
Contributor Author

@philipr-za philipr-za Jan 21, 2019

Choose a reason for hiding this comment

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

I found the check_closed_broadcast!(...) macro that seems to check that the final channel close broadcast message is sent and I replaced these calls with that.

let _ = handle_chan_reestablish_msgs!(nodes[1], nodes[0]);
let _ = nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]);
//Confirm the HTLC was ignored
assert_eq!(nodes[1].node.channel_state.lock().unwrap().by_id.get(&chan.2).unwrap().next_remote_htlc_id, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe also check the commitment_signed only has one HTLC in it?

Copy link
Contributor Author

@philipr-za philipr-za Jan 21, 2019

Choose a reason for hiding this comment

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

So I was implementing your suggestion but while I was looking around I realized that I am not sure if this requirement is actually being handled as written.

The requirement for the receiver says: BOLT 2 requirement: if the sender did not previously acknowledge the commitment of that HTLC: MUST ignore a repeated id value after a reconnection.

However, from what I can see is that if an HTLC is sent and the disconnect/reconnect happens before the commitment_dance is done and then a new HTLC message is sent with the same ID it is added rather than ignored and the original HTLC is discarded during the disconnect/reconnect process. This results in the same state I was testing for of there only being a single HTLC in the queue but it doesn't seem to be what the spec intends?

I would have thought that the first HTLC is retained and the repeated HTLC message should be ignored? Thoughts?

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 the spec needs changed there, then - the point of the disconnect/reconnect dance is we assume messages may have been lost in flight, so we definitely should be dropping the old ones and replacing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome, in that case we are good to go.

@philipr-za philipr-za force-pushed the philip-#129-tests-for-update-add-htlc branch from 54ed39d to 5473bd4 Compare January 21, 2019 12:49
@TheBlueMatt
Copy link
Collaborator

Will take as #296, you can see the list of changes at https://github.com/TheBlueMatt/rust-lightning/commits/2019-01-291-cleanups

@philipr-za philipr-za mentioned this pull request Jan 23, 2019
26 tasks
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