Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions fuzz/src/chanmon_consistency.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,7 +408,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {

loop {
macro_rules! send_payment {
($source: expr, $dest: expr) => { {
($source: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -417,15 +417,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
// Probably ran out of funds
test_return!();
}
} };
($source: expr, $middle: expr, $dest: expr) => { {
($source: expr, $middle: expr, $dest: expr, $amt: expr) => { {
let payment_hash = Sha256::hash(&[payment_id; 1]);
payment_id = payment_id.wrapping_add(1);
if let Err(_) = $source.send_payment(&Route {
Expand All@@ -441,7 +441,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
node_features: NodeFeatures::empty(),
short_channel_id: $dest.1,
channel_features: ChannelFeatures::empty(),
fee_msat: 5000000,
fee_msat: $amt,
cltv_expiry_delta: 200,
}]],
}, PaymentHash(payment_hash.into_inner()), &None) {
Expand DownExpand Up@@ -644,12 +644,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
for event in events.drain(..) {
match event {
events::Event::PaymentReceived { payment_hash, payment_secret, .. } => {
events::Event::PaymentReceived { payment_hash, payment_secret, amt } => {
if claim_set.insert(payment_hash.0) {
if $fail {
assert!(nodes[$node].fail_htlc_backwards(&payment_hash, &payment_secret));
} else {
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, 5_000_000));
assert!(nodes[$node].claim_funds(PaymentPreimage(payment_hash.0), &payment_secret, amt));
}
}
},
Expand DownExpand Up@@ -691,12 +691,12 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
nodes[2].channel_monitor_updated(&chan_2_funding, *id);
}
},
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a)),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a)),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b)),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b)),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x09 => send_payment!(nodes[0], (&nodes[1], chan_a), 5_000_000),
0x0a => send_payment!(nodes[1], (&nodes[0], chan_a), 5_000_000),
0x0b => send_payment!(nodes[1], (&nodes[2], chan_b), 5_000_000),
0x0c => send_payment!(nodes[2], (&nodes[1], chan_b), 5_000_000),
0x0d => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 5_000_000),
0x0e => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 5_000_000),
0x0f => {
if !chan_a_disconnected {
nodes[0].peer_disconnected(&nodes[1].get_our_node_id(), false);
Expand DownExpand Up@@ -781,6 +781,24 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
},
0x22 => send_payment_with_secret!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b)),
0x23 => send_payment_with_secret!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a)),
0x25 => send_payment!(nodes[0], (&nodes[1], chan_a), 10),
0x26 => send_payment!(nodes[1], (&nodes[0], chan_a), 10),
0x27 => send_payment!(nodes[1], (&nodes[2], chan_b), 10),
0x28 => send_payment!(nodes[2], (&nodes[1], chan_b), 10),
0x29 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 10),
0x2a => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 10),
0x2b => send_payment!(nodes[0], (&nodes[1], chan_a), 1_000),
0x2c => send_payment!(nodes[1], (&nodes[0], chan_a), 1_000),
0x2d => send_payment!(nodes[1], (&nodes[2], chan_b), 1_000),
0x2e => send_payment!(nodes[2], (&nodes[1], chan_b), 1_000),
0x2f => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 1_000),
0x30 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 1_000),
0x31 => send_payment!(nodes[0], (&nodes[1], chan_a), 100_000),
0x32 => send_payment!(nodes[1], (&nodes[0], chan_a), 100_000),
0x33 => send_payment!(nodes[1], (&nodes[2], chan_b), 100_000),
0x34 => send_payment!(nodes[2], (&nodes[1], chan_b), 100_000),
0x35 => send_payment!(nodes[0], (&nodes[1], chan_a), (&nodes[2], chan_b), 100_000),
0x36 => send_payment!(nodes[2], (&nodes[1], chan_b), (&nodes[0], chan_a), 100_000),
// 0x24 defined above
_ => test_return!(),
}
Expand Down
184 changes: 167 additions & 17 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@ pub struct ChannelValueStat {
pub pending_inbound_htlcs_amount_msat: u64,
pub holding_cell_outbound_amount_msat: u64,
pub their_max_htlc_value_in_flight_msat: u64, // outgoing
pub their_dust_limit_msat: u64,
}

enum InboundHTLCRemovalReason {
Expand DownExpand Up@@ -1663,8 +1664,83 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
cmp::min(self.value_to_self_msat as i64 - self.get_outbound_pending_htlc_stats().1 as i64, 0) as u64)
}

pub fn update_add_htlc(&mut self, msg: &msgs::UpdateAddHTLC, pending_forward_state: PendingHTLCStatus) -> Result<(), ChannelError> {
if (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32) {
// Get the fee cost of a commitment tx with a given number of HTLC outputs.
// Note that num_htlcs should not include dust HTLCs.
fn commit_tx_fee_msat(&self, num_htlcs: usize) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
(COMMITMENT_TX_BASE_WEIGHT + num_htlcs as u64 * COMMITMENT_TX_WEIGHT_PER_HTLC) * self.feerate_per_kw / 1000 * 1000
}

// Get the commitment tx fee for the local (i.e our) next commitment transaction
// based on the number of pending HTLCs that are on track to be in our next
// commitment tx. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust
// HTLCs are excluded.
fn next_local_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(self.channel_outbound);

let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.our_dust_limit_satoshis {

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.

Would it be preferable to multiple self.our_dust_limit_satoshis by 1000 instead to avoid losing precision?

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.

IIUC we want to be checking the rounded version since millisats are meaningless on-chain. Extrapolating from #577 (comment) that when we're dealing with amounts that relate to on-chain, always round down

continue
}
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
OutboundHTLCState::LocalAnnounced {..} => their_acked_htlcs += 1,
_ => {},
}
}

for htlc in self.holding_cell_htlc_updates.iter() {
match htlc {
&HTLCUpdateAwaitingACK::AddHTLC { .. } => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

// 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. `addl_htcs` is an optional parameter allowing the caller
// to add a number of additional HTLCs to the calculation. Note that dust HTLCs
// are excluded.
fn next_remote_commit_tx_fee_msat(&self, addl_htlcs: usize) -> u64 {
assert!(!self.channel_outbound);

// When calculating the set of HTLCs which will be included in their next
Comment thread
valentinewallace marked this conversation as resolved.
// commitment_signed, all inbound HTLCs are included (as all states imply it will be
// included) and only committed outbound HTLCs, see below.
let mut their_acked_htlcs = self.pending_inbound_htlcs.len();
for ref htlc in self.pending_outbound_htlcs.iter() {
if htlc.amount_msat / 1000 <= self.their_dust_limit_satoshis {
continue
}
// We only include outbound HTLCs if it will not be included in their next
// commitment_signed, i.e. if they've responded to us with an RAA after announcement.
match htlc.state {
OutboundHTLCState::Committed => their_acked_htlcs += 1,
OutboundHTLCState::RemoteRemoved {..} => their_acked_htlcs += 1,
_ => {},
}
}

self.commit_tx_fee_msat(their_acked_htlcs + addl_htlcs)
}

pub fn update_add_htlc<F>(&mut self, msg: &msgs::UpdateAddHTLC, mut pending_forward_status: PendingHTLCStatus, create_pending_htlc_status: F) -> Result<(), ChannelError>
where F: for<'a> Fn(&'a Self, PendingHTLCStatus, u16) -> PendingHTLCStatus {
// We can't accept HTLCs sent after we've sent a shutdown.
let local_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::LocalShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if local_sent_shutdown {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why assign product of channel state comparison and then test this assignment instead of testing channel state directly ?

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.

Just to make it readable and clear what's being checked. The variable makes it explicit that we're trying to check whether the local has sent a shutdown msg

pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|20);
}
// If the remote has sent a shutdown prior to adding this HTLC, then they are in violation of the spec.
let remote_sent_shutdown = (self.channel_state & (ChannelState::ChannelFunded as u32 | ChannelState::RemoteShutdownSent as u32)) != (ChannelState::ChannelFunded as u32);
if remote_sent_shutdown {
return Err(ChannelError::Close("Got add HTLC message when channel was not in an operational state"));
}
if self.channel_state & (ChannelState::PeerDisconnected as u32) == ChannelState::PeerDisconnected as u32 {
Expand DownExpand Up@@ -1708,9 +1784,55 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
removed_outbound_total_msat += htlc.amount_msat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TheBlueMatt

I don't get the rational between the difference in the way we account inbound HTLCs and outbound HTLCs. For any inbound HTLCs we evaluate as part of our balance independently of its state. We presume its resolution is favorable to us, even if it maybe not the case, which is reasonable without more tracking complexity.

Instead for outbound HTLC we account them in counterparty balance only after its removal announcement, independently of a success or failure. Why we don't also presume outbound HTLCs resolution are always favorable to them ? I.e accounting in their balance once we announce outbound HTLCs

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.

I dont really get your question. We should not be assuming any kind of HTLC resolution here, nor do I think we do - we only ever consider HTLCs which have begun the removal process.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we talked about this offline but after reading conv again it was a still a misunderstood. I think we're making a different assumption for outbound, namely we don't consider that a committed outbound will resolve favorably for them.

Why we don't account OutboundHTLCState::Committed in part of their balance, removed_outbound_total_msat ?

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.

I'm still a bit confused. Note that the "None" here means "there is no HTLCFailReason and the claim has resolved in our favor"

}
}
if htlc_inbound_value_msat + msg.amount_msat + self.value_to_self_msat > (self.channel_value_satoshis - Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis)) * 1000 + removed_outbound_total_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under their reserve value"));

let pending_value_to_self_msat =
self.value_to_self_msat + htlc_inbound_value_msat - removed_outbound_total_msat;
let pending_remote_value_msat =
self.channel_value_satoshis * 1000 - pending_value_to_self_msat;
if pending_remote_value_msat < msg.amount_msat {
return Err(ChannelError::Close("Remote HTLC add would overdraw remaining funds"));
}

// Check that the remote can afford to pay for this HTLC on-chain at the current
// feerate_per_kw, while maintaining their channel reserve (as required by the spec).
let remote_commit_tx_fee_msat = if self.channel_outbound { 0 } else {
// +1 for this HTLC.
self.next_remote_commit_tx_fee_msat(1)
};
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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the spec deviation right ? Please at least add a comment for it and modify commit message to say we fail channel. Anyway that may not conjugate well with anchor output, where once implemented a initiator-sender consider it's always good to send a HTLC if above reserve because you can always bump ?

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.

Sorry, don't believe this is a deviation. Curious why you think it's a deviation?
From the BOLT 2:

update_add_htlc
A sending node:
if it is responsible for paying the Bitcoin fee:
MUST NOT offer amount_msat if, after adding that HTLC to its commitment transaction, it cannot pay the fee for either the local or remote commitment transaction at the current feerate_per_kw while maintaining its channel reserve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AFAICT, this is a requirement on the sender but here we're at HTLC reception. It makes sense as that way you avoid to close channel even if sender violates spec. I think channel can still be unstuck if this HTLC is failed and sender-initiator balance is increased again without waiting feerate decrease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

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.

Ah but there's no fee spike buffer check here?

Here's the deviating fee spike buffer check: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1811-R1818

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.

Maybe misunderstood is due to the spec saying

update_add_htlc
A receiving node:
receiving an amount_msat that the sending node cannot afford at the current `feerate_per_kw` (while maintaining its channel reserve):
SHOULD fail the channel

It doesn't require to enforce fee buffer spikes.

Right, this check is intended to purely perform the check that you quote above here ^

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After IRC discussion, this is right, I confused myself with what this check was enforcing, but still you should have point out the receiver spec-side :p

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.

That's fair, lol

@valentinewallacevalentinewallaceJun 12, 2020

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.

};

let chan_reserve_msat =
Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis) * 1000;
if pending_remote_value_msat - msg.amount_msat - remote_commit_tx_fee_msat < chan_reserve_msat {
return Err(ChannelError::Close("Remote HTLC add would put them under remote reserve value"));
}

if !self.channel_outbound {
// `+1` for this HTLC, `2 *` and `+1` fee spike buffer we keep for the remote. This deviates from the
// spec because in the spec, the fee spike buffer requirement doesn't exist on the receiver's side,
// only on the sender's.
// Note that when we eventually remove support for fee updates and switch to anchor output fees,
// we will drop the `2 *`, since we no longer be as sensitive to fee spikes. But, keep the extra +1
// as we should still be able to afford adding this HTLC plus one more future HTLC, regardless of
// being sensitive to fee spikes.
let remote_fee_cost_incl_stuck_buffer_msat = 2 * self.next_remote_commit_tx_fee_msat(1 + 1);
if pending_remote_value_msat - msg.amount_msat - chan_reserve_msat < remote_fee_cost_incl_stuck_buffer_msat {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we don't check that sender-initiator can also afford this new HTLC with regards to our local commitment transaction fee ?

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.

That's what this is supposed to be doing: https://github.com/rust-bitcoin/rust-lightning/pull/577/files#diff-79ec46664bd0cf85b6be0ddb567527a7R1830. Though it also takes into account the channel reserve, which we discuss further below.

// Note that if the pending_forward_status is not updated here, then it's because we're already failing
// the HTLC, i.e. its status is already set to failing.
pending_forward_status = create_pending_htlc_status(self, pending_forward_status, 0x1000|7);
}
} else {
// Check that they won't violate our local required channel reserve by adding this HTLC.

// +1 for this HTLC.
let local_commit_tx_fee_msat = self.next_local_commit_tx_fee_msat(1);

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.

If you set this to 0 the tests all still pass.

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.

Hm, when I set this to 0 test_chan_reserve_violation_inbound_htlc_outbound_channel fails (as is the intention of the test). Would you mind double checking?

if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + local_commit_tx_fee_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Close("Cannot receive value that would put us under local channel reserve value"));
}
}

if self.next_remote_htlc_id != msg.htlc_id {
return Err(ChannelError::Close("Remote skipped HTLC ID"));
}
Expand All@@ -1719,7 +1841,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
}

if self.channel_state & ChannelState::LocalShutdownSent as u32 != 0 {
if let PendingHTLCStatus::Forward(_) = pending_forward_state {
if let PendingHTLCStatus::Forward(_) = pending_forward_status {
panic!("ChannelManager shouldn't be trying to add a forwardable HTLC after we've started closing");
}
}
Expand All@@ -1731,7 +1853,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
amount_msat: msg.amount_msat,
payment_hash: msg.payment_hash,
cltv_expiry: msg.cltv_expiry,
state: InboundHTLCState::RemoteAnnounced(pending_forward_state),
state: InboundHTLCState::RemoteAnnounced(pending_forward_status),
});
Ok(())
}
Expand DownExpand Up@@ -3036,6 +3158,7 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
res
},
their_max_htlc_value_in_flight_msat: self.their_max_htlc_value_in_flight_msat,
their_dust_limit_msat: self.their_dust_limit_satoshis * 1000,
}
}

Expand DownExpand Up@@ -3546,40 +3669,67 @@ impl<ChanSigner: ChannelKeys> Channel<ChanSigner> {
return Err(ChannelError::Ignore("Cannot send value that would put us over the max HTLC value in flight our peer will accept"));
}

if !self.channel_outbound {
// Check that we won't violate the remote channel reserve by adding this HTLC.

let remote_balance_msat = self.channel_value_satoshis * 1000 - self.value_to_self_msat;
let remote_chan_reserve_msat = Channel::<ChanSigner>::get_remote_channel_reserve_satoshis(self.channel_value_satoshis);
// 1 additional HTLC corresponding to this HTLC.
let remote_commit_tx_fee_msat = self.next_remote_commit_tx_fee_msat(1);
if remote_balance_msat < remote_chan_reserve_msat + remote_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would put them under remote channel reserve value"));

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.

Is this scenario tested?

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.

Added here: f26a2d2 though not certain it's the best way of testing, same with the other tests that I added.

}
}

let pending_value_to_self_msat = self.value_to_self_msat - htlc_outbound_value_msat;
if pending_value_to_self_msat < amount_msat {
return Err(ChannelError::Ignore("Cannot send value that would overdraw remaining funds"));
}

// The `+1` is for the HTLC currently being added to the commitment tx and
// the `2 *` and `+1` are for the fee spike buffer.
let local_commit_tx_fee_msat = if self.channel_outbound {
2 * self.next_local_commit_tx_fee_msat(1 + 1)
} else { 0 };
if pending_value_to_self_msat - amount_msat < local_commit_tx_fee_msat {
return Err(ChannelError::Ignore("Cannot send value that would not leave enough to pay for fees"));
}

// Check self.local_channel_reserve_satoshis (the amount we must keep as
// reserve for the remote to have something to claim if we misbehave)
if self.value_to_self_msat < self.local_channel_reserve_satoshis * 1000 + amount_msat + htlc_outbound_value_msat {
let chan_reserve_msat = self.local_channel_reserve_satoshis * 1000;
if pending_value_to_self_msat - amount_msat - local_commit_tx_fee_msat < chan_reserve_msat {
Comment thread
valentinewallace marked this conversation as resolved.
return Err(ChannelError::Ignore("Cannot send value that would put us under local channel reserve value"));
}

// Now update local state:
if (self.channel_state & (ChannelState::AwaitingRemoteRevoke as u32)) == (ChannelState::AwaitingRemoteRevoke as u32) {
self.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::AddHTLC {
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
amount_msat,
Comment thread
valentinewallace marked this conversation as resolved.
payment_hash,
cltv_expiry,
source,
onion_routing_packet: onion_routing_packet,
onion_routing_packet,
});
return Ok(None);
}

self.pending_outbound_htlcs.push(OutboundHTLCOutput {
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
amount_msat,
payment_hash: payment_hash.clone(),
cltv_expiry: cltv_expiry,
cltv_expiry,
state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())),
source,
});

let res = msgs::UpdateAddHTLC {
channel_id: self.channel_id,
htlc_id: self.next_local_htlc_id,
amount_msat: amount_msat,
payment_hash: payment_hash,
cltv_expiry: cltv_expiry,
onion_routing_packet: onion_routing_packet,
amount_msat,
payment_hash,
cltv_expiry,
onion_routing_packet,
};
self.next_local_htlc_id += 1;

Expand Down
Loading