Skip to content

Commit 05e6252

Browse files
committed
Implement non-strict forwarding
This change implements non-strict forwarding, allowing the node to forward an HTLC along a channel other than the one specified by short_channel_id in the onion message, so long as the receiver has the same node public key intended by short_channel_id ([BOLT](https://github.com/lightning/bolts/blob/57ce4b1e05c996fa649f00dc13521f6d496a288f/04-onion-routing.md#non-strict-forwarding)). This can improve payment reliability when there are multiple channels with the same peer e.g. when outbound liquidity is replenished by opening a new channel. The implemented forwarding strategy now chooses the channel with the lowest outbound liquidity that can forward an HTLC to maximize the probability of being able to successfully forward a subsequent HTLC. Fixes #1278.
1 parent 07f3380 commit 05e6252

File tree

3 files changed

+230
-80
lines changed

3 files changed

+230
-80
lines changed

fuzz/src/full_stack.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,8 +1366,8 @@ mod tests {
13661366

13671367
// process the now-pending HTLC forward
13681368
ext_from_hex("07", &mut test);
1369-
// Two feerate requests to check dust exposure
1370-
ext_from_hex("00fd00fd", &mut test);
1369+
// Three feerate requests to check dust exposure
1370+
ext_from_hex("00fd00fd00fd", &mut test);
13711371
// client now sends id 1 update_add_htlc and commitment_signed (CHECK 7: UpdateHTLCs event for node 03020000 with 1 HTLCs for channel 3f000000)
13721372

13731373
// we respond with commitment_signed then revoke_and_ack (a weird, but valid, order)
@@ -1478,8 +1478,8 @@ mod tests {
14781478
// process the now-pending HTLC forward
14791479
ext_from_hex("07", &mut test);
14801480

1481-
// Two feerate requests to check dust exposure
1482-
ext_from_hex("00fd00fd", &mut test);
1481+
// Three feerate requests to check dust exposure
1482+
ext_from_hex("00fd00fd00fd", &mut test);
14831483

14841484
// client now sends id 1 update_add_htlc and commitment_signed (CHECK 7 duplicate)
14851485
// we respond with revoke_and_ack, then commitment_signed, then update_fail_htlc
@@ -1602,8 +1602,8 @@ mod tests {
16021602

16031603
// process the now-pending HTLC forward
16041604
ext_from_hex("07", &mut test);
1605-
// Two feerate requests to check dust exposure
1606-
ext_from_hex("00fd00fd", &mut test);
1605+
// Three feerate requests to check dust exposure
1606+
ext_from_hex("00fd00fd00fd", &mut test);
16071607
// client now sends id 1 update_add_htlc and commitment_signed (CHECK 7 duplicate)
16081608

16091609
// connect a block with one transaction of len 125

lightning/src/ln/channelmanager.rs

Lines changed: 121 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4895,8 +4895,8 @@ where
48954895
if short_chan_id != 0 {
48964896
let mut forwarding_counterparty = None;
48974897
macro_rules! forwarding_channel_not_found {
4898-
() => {
4899-
for forward_info in pending_forwards.drain(..) {
4898+
($forward_infos: expr) => {
4899+
for forward_info in $forward_infos {
49004900
match forward_info {
49014901
HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo {
49024902
prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint,
@@ -5004,104 +5004,156 @@ where
50045004
let (counterparty_node_id, forward_chan_id) = match chan_info_opt {
50055005
Some((cp_id, chan_id)) => (cp_id, chan_id),
50065006
None => {
5007-
forwarding_channel_not_found!();
5007+
forwarding_channel_not_found!(pending_forwards.drain(..));
50085008
continue;
50095009
}
50105010
};
50115011
forwarding_counterparty = Some(counterparty_node_id);
50125012
let per_peer_state = self.per_peer_state.read().unwrap();
50135013
let peer_state_mutex_opt = per_peer_state.get(&counterparty_node_id);
50145014
if peer_state_mutex_opt.is_none() {
5015-
forwarding_channel_not_found!();
5015+
forwarding_channel_not_found!(pending_forwards.drain(..));
50165016
continue;
50175017
}
50185018
let mut peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap();
50195019
let peer_state = &mut *peer_state_lock;
5020-
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
5021-
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
5022-
for forward_info in pending_forwards.drain(..) {
5023-
let queue_fail_htlc_res = match forward_info {
5024-
HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo {
5025-
prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint,
5026-
prev_user_channel_id, forward_info: PendingHTLCInfo {
5027-
incoming_shared_secret, payment_hash, outgoing_amt_msat, outgoing_cltv_value,
5028-
routing: PendingHTLCRouting::Forward {
5029-
onion_packet, blinded, ..
5030-
}, skimmed_fee_msat, ..
5020+
let mut draining_pending_forwards = pending_forwards.drain(..);
5021+
while let Some(forward_info) = draining_pending_forwards.next() {
5022+
let queue_fail_htlc_res = match forward_info {
5023+
HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo {
5024+
prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint,
5025+
prev_user_channel_id, forward_info: PendingHTLCInfo {
5026+
incoming_shared_secret, payment_hash, outgoing_amt_msat, outgoing_cltv_value,
5027+
routing: PendingHTLCRouting::Forward {
5028+
ref onion_packet, blinded, ..
5029+
}, skimmed_fee_msat, ..
5030+
},
5031+
}) => {
5032+
let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
5033+
short_channel_id: prev_short_channel_id,
5034+
user_channel_id: Some(prev_user_channel_id),
5035+
channel_id: prev_channel_id,
5036+
outpoint: prev_funding_outpoint,
5037+
htlc_id: prev_htlc_id,
5038+
incoming_packet_shared_secret: incoming_shared_secret,
5039+
// Phantom payments are only PendingHTLCRouting::Receive.
5040+
phantom_shared_secret: None,
5041+
blinded_failure: blinded.map(|b| b.failure),
5042+
});
5043+
let next_blinding_point = blinded.and_then(|b| {
5044+
let encrypted_tlvs_ss = self.node_signer.ecdh(
5045+
Recipient::Node, &b.inbound_blinding_point, None
5046+
).unwrap().secret_bytes();
5047+
onion_utils::next_hop_pubkey(
5048+
&self.secp_ctx, b.inbound_blinding_point, &encrypted_tlvs_ss
5049+
).ok()
5050+
});
5051+
5052+
// Forward the HTLC over the most appropriate channel with the corresponding peer,
5053+
// applying non-strict forwarding.
5054+
// The channel with the least amount of outbound liquidity will be used to maximize the
5055+
// probability of being able to successfully forward a subsequent HTLC.
5056+
let maybe_optimal_channel = peer_state.channel_by_id.values_mut().filter_map(|phase| match phase {
5057+
ChannelPhase::Funded(chan) => {
5058+
let balances = chan.context.get_available_balances(&self.fee_estimator);
5059+
if outgoing_amt_msat <= balances.next_outbound_htlc_limit_msat &&
5060+
outgoing_amt_msat >= balances.next_outbound_htlc_minimum_msat &&
5061+
chan.context.is_usable() {
5062+
Some((chan, balances))
5063+
} else {
5064+
None
5065+
}
50315066
},
5032-
}) => {
5033-
let logger = WithChannelContext::from(&self.logger, &chan.context, Some(payment_hash));
5034-
log_trace!(logger, "Adding HTLC from short id {} with payment_hash {} to channel with short id {} after delay", prev_short_channel_id, &payment_hash, short_chan_id);
5035-
let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
5036-
short_channel_id: prev_short_channel_id,
5037-
user_channel_id: Some(prev_user_channel_id),
5038-
channel_id: prev_channel_id,
5039-
outpoint: prev_funding_outpoint,
5040-
htlc_id: prev_htlc_id,
5041-
incoming_packet_shared_secret: incoming_shared_secret,
5042-
// Phantom payments are only PendingHTLCRouting::Receive.
5043-
phantom_shared_secret: None,
5044-
blinded_failure: blinded.map(|b| b.failure),
5045-
});
5046-
let next_blinding_point = blinded.and_then(|b| {
5047-
let encrypted_tlvs_ss = self.node_signer.ecdh(
5048-
Recipient::Node, &b.inbound_blinding_point, None
5049-
).unwrap().secret_bytes();
5050-
onion_utils::next_hop_pubkey(
5051-
&self.secp_ctx, b.inbound_blinding_point, &encrypted_tlvs_ss
5052-
).ok()
5053-
});
5054-
if let Err(e) = chan.queue_add_htlc(outgoing_amt_msat,
5055-
payment_hash, outgoing_cltv_value, htlc_source.clone(),
5056-
onion_packet, skimmed_fee_msat, next_blinding_point, &self.fee_estimator,
5057-
&&logger)
5058-
{
5059-
if let ChannelError::Ignore(msg) = e {
5060-
log_trace!(logger, "Failed to forward HTLC with payment_hash {}: {}", &payment_hash, msg);
5067+
_ => None,
5068+
}).min_by_key(|(_, balances)| balances.next_outbound_htlc_limit_msat).map(|(c, _)| c);
5069+
let optimal_channel = match maybe_optimal_channel {
5070+
Some(chan) => chan,
5071+
None => {
5072+
// Fall back to the specified channel to return an appropriate error.
5073+
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
5074+
chan
50615075
} else {
5062-
panic!("Stated return value requirements in send_htlc() were not met");
5076+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
5077+
break;
50635078
}
5079+
}
5080+
};
5081+
5082+
let logger = WithChannelContext::from(&self.logger, &optimal_channel.context, Some(payment_hash));
5083+
let channel_description = if optimal_channel.context.get_short_channel_id() == Some(short_chan_id) {
5084+
"specified"
5085+
} else {
5086+
"alternate"
5087+
};
5088+
log_trace!(logger, "Forwarding HTLC from SCID {} with payment_hash {} and next hop SCID {} over {} channel {} with corresponding peer {}",
5089+
prev_short_channel_id, &payment_hash, short_chan_id, channel_description, optimal_channel.context.channel_id(), &counterparty_node_id);
5090+
if let Err(e) = optimal_channel.queue_add_htlc(outgoing_amt_msat,
5091+
payment_hash, outgoing_cltv_value, htlc_source.clone(),
5092+
onion_packet.clone(), skimmed_fee_msat, next_blinding_point, &self.fee_estimator,
5093+
&&logger)
5094+
{
5095+
if let ChannelError::Ignore(msg) = e {
5096+
log_trace!(logger, "Failed to forward HTLC with payment_hash {} to peer {}: {}", &payment_hash, &counterparty_node_id, msg);
5097+
} else {
5098+
panic!("Stated return value requirements in send_htlc() were not met");
5099+
}
5100+
5101+
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
50645102
let (failure_code, data) = self.get_htlc_temp_fail_err_and_data(0x1000|7, short_chan_id, chan);
50655103
failed_forwards.push((htlc_source, payment_hash,
50665104
HTLCFailReason::reason(failure_code, data),
50675105
HTLCDestination::NextHopChannel { node_id: Some(chan.context.get_counterparty_node_id()), channel_id: forward_chan_id }
50685106
));
5069-
continue;
5107+
} else {
5108+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
5109+
break;
50705110
}
5071-
None
5072-
},
5073-
HTLCForwardInfo::AddHTLC { .. } => {
5074-
panic!("short_channel_id != 0 should imply any pending_forward entries are of type Forward");
5075-
},
5076-
HTLCForwardInfo::FailHTLC { htlc_id, err_packet } => {
5111+
}
5112+
None
5113+
},
5114+
HTLCForwardInfo::AddHTLC { .. } => {
5115+
panic!("short_channel_id != 0 should imply any pending_forward entries are of type Forward");
5116+
},
5117+
HTLCForwardInfo::FailHTLC { htlc_id, ref err_packet } => {
5118+
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
5119+
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
50775120
log_trace!(logger, "Failing HTLC back to channel with short id {} (backward HTLC ID {}) after delay", short_chan_id, htlc_id);
5078-
Some((chan.queue_fail_htlc(htlc_id, err_packet, &&logger), htlc_id))
5079-
},
5080-
HTLCForwardInfo::FailMalformedHTLC { htlc_id, failure_code, sha256_of_onion } => {
5121+
Some((chan.queue_fail_htlc(htlc_id, err_packet.clone(), &&logger), htlc_id))
5122+
} else {
5123+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
5124+
break;
5125+
}
5126+
},
5127+
HTLCForwardInfo::FailMalformedHTLC { htlc_id, failure_code, sha256_of_onion } => {
5128+
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
5129+
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
50815130
log_trace!(logger, "Failing malformed HTLC back to channel with short id {} (backward HTLC ID {}) after delay", short_chan_id, htlc_id);
50825131
let res = chan.queue_fail_malformed_htlc(
50835132
htlc_id, failure_code, sha256_of_onion, &&logger
50845133
);
50855134
Some((res, htlc_id))
5086-
},
5087-
};
5088-
if let Some((queue_fail_htlc_res, htlc_id)) = queue_fail_htlc_res {
5089-
if let Err(e) = queue_fail_htlc_res {
5090-
if let ChannelError::Ignore(msg) = e {
5135+
} else {
5136+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
5137+
break;
5138+
}
5139+
},
5140+
};
5141+
if let Some((queue_fail_htlc_res, htlc_id)) = queue_fail_htlc_res {
5142+
if let Err(e) = queue_fail_htlc_res {
5143+
if let ChannelError::Ignore(msg) = e {
5144+
if let Some(ChannelPhase::Funded(ref mut chan)) = peer_state.channel_by_id.get_mut(&forward_chan_id) {
5145+
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
50915146
log_trace!(logger, "Failed to fail HTLC with ID {} backwards to short_id {}: {}", htlc_id, short_chan_id, msg);
5092-
} else {
5093-
panic!("Stated return value requirements in queue_fail_{{malformed_}}htlc() were not met");
50945147
}
5095-
// fail-backs are best-effort, we probably already have one
5096-
// pending, and if not that's OK, if not, the channel is on
5097-
// the chain and sending the HTLC-Timeout is their problem.
5098-
continue;
5148+
} else {
5149+
panic!("Stated return value requirements in queue_fail_{{malformed_}}htlc() were not met");
50995150
}
5151+
// fail-backs are best-effort, we probably already have one
5152+
// pending, and if not that's OK, if not, the channel is on
5153+
// the chain and sending the HTLC-Timeout is their problem.
5154+
continue;
51005155
}
51015156
}
5102-
} else {
5103-
forwarding_channel_not_found!();
5104-
continue;
51055157
}
51065158
} else {
51075159
'next_forwardable_htlc: for forward_info in pending_forwards.drain(..) {

0 commit comments

Comments
 (0)