// our payment, which we can use to decode errors or inform the user that the payment was sent.
#[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug
-enum PendingHTLCRouting {
+pub(super) enum PendingHTLCRouting {
Forward {
onion_packet: msgs::OnionPacket,
short_channel_id: u64, // This should be NonZero<u64> eventually when we bump MSRV
Receive {
payment_data: msgs::FinalOnionHopData,
incoming_cltv_expiry: u32, // Used to track when we should expire pending HTLCs that go unclaimed
+ phantom_shared_secret: Option<[u8; 32]>,
},
ReceiveKeysend {
payment_preimage: PaymentPreimage,
#[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug
pub(super) struct PendingHTLCInfo {
- routing: PendingHTLCRouting,
- incoming_shared_secret: [u8; 32],
+ pub(super) routing: PendingHTLCRouting,
+ pub(super) incoming_shared_secret: [u8; 32],
payment_hash: PaymentHash,
pub(super) amt_to_forward: u64,
pub(super) outgoing_cltv_value: u32,
short_channel_id: u64,
htlc_id: u64,
incoming_packet_shared_secret: [u8; 32],
+ phantom_shared_secret: Option<[u8; 32]>,
// This field is consumed by `claim_funds_from_hop()` when updating a force-closed backwards
// channel with a preimage provided by the forward channel.
let mut channel_state = self.channel_state.lock().unwrap();
match channel_state.by_id.entry(temporary_channel_id) {
hash_map::Entry::Occupied(_) => {
- if cfg!(feature = "fuzztarget") {
+ if cfg!(fuzzing) {
return Err(APIError::APIMisuseError { err: "Fuzzy bad RNG".to_owned() });
} else {
panic!("RNG is bad???");
}
fn construct_recv_pending_htlc_info(&self, hop_data: msgs::OnionHopData, shared_secret: [u8; 32],
- payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32) -> Result<PendingHTLCInfo, ReceiveError>
+ payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>) -> Result<PendingHTLCInfo, ReceiveError>
{
// final_incorrect_cltv_expiry
if hop_data.outgoing_cltv_value != cltv_expiry {
PendingHTLCRouting::Receive {
payment_data: data,
incoming_cltv_expiry: hop_data.outgoing_cltv_value,
+ phantom_shared_secret,
}
} else if let Some(payment_preimage) = keysend_preimage {
// We need to check that the sender knows the keysend preimage before processing this
let pending_forward_info = match next_hop {
onion_utils::Hop::Receive(next_hop_data) => {
// OUR PAYMENT!
- match self.construct_recv_pending_htlc_info(next_hop_data, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry) {
+ match self.construct_recv_pending_htlc_info(next_hop_data, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, None) {
Ok(info) => {
// Note that we could obviously respond immediately with an update_fulfill_htlc
// message, however that would leak that we are the recipient of this payment, so
if let &PendingHTLCRouting::Forward { ref short_channel_id, .. } = routing {
let id_option = channel_state.as_ref().unwrap().short_to_id.get(&short_channel_id).cloned();
if let Some((err, code, chan_update)) = loop {
- let forwarding_id = match id_option {
+ let forwarding_id_opt = match id_option {
None => { // unknown_next_peer
- break Some(("Don't have available channel for forwarding as requested.", 0x4000 | 10, None));
+ // Note that this is likely a timing oracle for detecting whether an scid is a
+ // phantom.
+ if fake_scid::is_valid_phantom(&self.fake_scid_rand_bytes, *short_channel_id) {
+ None
+ } else {
+ break Some(("Don't have available channel for forwarding as requested.", 0x4000 | 10, None));
+ }
},
- Some(id) => id.clone(),
+ Some(id) => Some(id.clone()),
};
+ let (chan_update_opt, forwardee_cltv_expiry_delta) = if let Some(forwarding_id) = forwarding_id_opt {
+ let chan = channel_state.as_mut().unwrap().by_id.get_mut(&forwarding_id).unwrap();
+ // Leave channel updates as None for private channels.
+ let chan_update_opt = if chan.should_announce() {
+ Some(self.get_channel_update_for_unicast(chan).unwrap()) } else { None };
+ if !chan.should_announce() && !self.default_configuration.accept_forwards_to_priv_channels {
+ // Note that the behavior here should be identical to the above block - we
+ // should NOT reveal the existence or non-existence of a private channel if
+ // we don't allow forwards outbound over them.
+ break Some(("Don't have available channel for forwarding as requested.", 0x4000 | 10, None));
+ }
- let chan = channel_state.as_mut().unwrap().by_id.get_mut(&forwarding_id).unwrap();
-
- if !chan.should_announce() && !self.default_configuration.accept_forwards_to_priv_channels {
- // Note that the behavior here should be identical to the above block - we
- // should NOT reveal the existence or non-existence of a private channel if
- // we don't allow forwards outbound over them.
- break Some(("Don't have available channel for forwarding as requested.", 0x4000 | 10, None));
- }
+ // Note that we could technically not return an error yet here and just hope
+ // that the connection is reestablished or monitor updated by the time we get
+ // around to doing the actual forward, but better to fail early if we can and
+ // hopefully an attacker trying to path-trace payments cannot make this occur
+ // on a small/per-node/per-channel scale.
+ if !chan.is_live() { // channel_disabled
+ break Some(("Forwarding channel is not in a ready state.", 0x1000 | 20, chan_update_opt));
+ }
+ if *amt_to_forward < chan.get_counterparty_htlc_minimum_msat() { // amount_below_minimum
+ break Some(("HTLC amount was below the htlc_minimum_msat", 0x1000 | 11, chan_update_opt));
+ }
+ let fee = amt_to_forward.checked_mul(chan.get_fee_proportional_millionths() as u64)
+ .and_then(|prop_fee| { (prop_fee / 1000000)
+ .checked_add(chan.get_outbound_forwarding_fee_base_msat() as u64) });
+ if fee.is_none() || msg.amount_msat < fee.unwrap() || (msg.amount_msat - fee.unwrap()) < *amt_to_forward { // fee_insufficient
+ break Some(("Prior hop has deviated from specified fees parameters or origin node has obsolete ones", 0x1000 | 12, chan_update_opt));
+ }
+ (chan_update_opt, chan.get_cltv_expiry_delta())
+ } else { (None, MIN_CLTV_EXPIRY_DELTA) };
- // Note that we could technically not return an error yet here and just hope
- // that the connection is reestablished or monitor updated by the time we get
- // around to doing the actual forward, but better to fail early if we can and
- // hopefully an attacker trying to path-trace payments cannot make this occur
- // on a small/per-node/per-channel scale.
- if !chan.is_live() { // channel_disabled
- break Some(("Forwarding channel is not in a ready state.", 0x1000 | 20, Some(self.get_channel_update_for_unicast(chan).unwrap())));
- }
- if *amt_to_forward < chan.get_counterparty_htlc_minimum_msat() { // amount_below_minimum
- break Some(("HTLC amount was below the htlc_minimum_msat", 0x1000 | 11, Some(self.get_channel_update_for_unicast(chan).unwrap())));
- }
- let fee = amt_to_forward.checked_mul(chan.get_fee_proportional_millionths() as u64)
- .and_then(|prop_fee| { (prop_fee / 1000000)
- .checked_add(chan.get_outbound_forwarding_fee_base_msat() as u64) });
- if fee.is_none() || msg.amount_msat < fee.unwrap() || (msg.amount_msat - fee.unwrap()) < *amt_to_forward { // fee_insufficient
- break Some(("Prior hop has deviated from specified fees parameters or origin node has obsolete ones", 0x1000 | 12, Some(self.get_channel_update_for_unicast(chan).unwrap())));
- }
- if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + chan.get_cltv_expiry_delta() as u64 { // incorrect_cltv_expiry
- break Some(("Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta", 0x1000 | 13, Some(self.get_channel_update_for_unicast(chan).unwrap())));
+ if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + forwardee_cltv_expiry_delta as u64 { // incorrect_cltv_expiry
+ break Some(("Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta", 0x1000 | 13, chan_update_opt));
}
let cur_height = self.best_block.read().unwrap().height() + 1;
// Theoretically, channel counterparty shouldn't send us a HTLC expiring now,
// but we want to be robust wrt to counterparty packet sanitization (see
// HTLC_FAIL_BACK_BUFFER rationale).
if msg.cltv_expiry <= cur_height + HTLC_FAIL_BACK_BUFFER as u32 { // expiry_too_soon
- break Some(("CLTV expiry is too close", 0x1000 | 14, Some(self.get_channel_update_for_unicast(chan).unwrap())));
+ break Some(("CLTV expiry is too close", 0x1000 | 14, chan_update_opt));
}
if msg.cltv_expiry > cur_height + CLTV_FAR_FAR_AWAY as u32 { // expiry_too_far
break Some(("CLTV expiry is too far in the future", 21, None));
// but there is no need to do that, and since we're a bit conservative with our
// risk threshold it just results in failing to forward payments.
if (*outgoing_cltv_value) as u64 <= (cur_height + LATENCY_GRACE_PERIOD_BLOCKS) as u64 {
- break Some(("Outgoing CLTV value is too soon", 0x1000 | 14, Some(self.get_channel_update_for_unicast(chan).unwrap())));
+ break Some(("Outgoing CLTV value is too soon", 0x1000 | 14, chan_update_opt));
}
break None;
let mut new_events = Vec::new();
let mut failed_forwards = Vec::new();
+ let mut phantom_receives: Vec<(u64, OutPoint, Vec<(PendingHTLCInfo, u64)>)> = Vec::new();
let mut handle_errors = Vec::new();
{
let mut channel_state_lock = self.channel_state.lock().unwrap();
let forward_chan_id = match channel_state.short_to_id.get(&short_chan_id) {
Some(chan_id) => chan_id.clone(),
None => {
- failed_forwards.reserve(pending_forwards.len());
for forward_info in pending_forwards.drain(..) {
match forward_info {
- HTLCForwardInfo::AddHTLC { prev_short_channel_id, prev_htlc_id, forward_info,
- prev_funding_outpoint } => {
- let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
- short_channel_id: prev_short_channel_id,
- outpoint: prev_funding_outpoint,
- htlc_id: prev_htlc_id,
- incoming_packet_shared_secret: forward_info.incoming_shared_secret,
- });
- failed_forwards.push((htlc_source, forward_info.payment_hash,
- HTLCFailReason::Reason { failure_code: 0x4000 | 10, data: Vec::new() }
- ));
- },
+ HTLCForwardInfo::AddHTLC { prev_short_channel_id, prev_htlc_id, forward_info: PendingHTLCInfo {
+ routing, incoming_shared_secret, payment_hash, amt_to_forward, outgoing_cltv_value },
+ prev_funding_outpoint } => {
+ macro_rules! fail_forward {
+ ($msg: expr, $err_code: expr, $err_data: expr, $phantom_ss: expr) => {
+ {
+ log_info!(self.logger, "Failed to accept/forward incoming HTLC: {}", $msg);
+ let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
+ short_channel_id: prev_short_channel_id,
+ outpoint: prev_funding_outpoint,
+ htlc_id: prev_htlc_id,
+ incoming_packet_shared_secret: incoming_shared_secret,
+ phantom_shared_secret: $phantom_ss,
+ });
+ failed_forwards.push((htlc_source, payment_hash,
+ HTLCFailReason::Reason { failure_code: $err_code, data: $err_data }
+ ));
+ continue;
+ }
+ }
+ }
+ if let PendingHTLCRouting::Forward { onion_packet, .. } = routing {
+ let phantom_secret_res = self.keys_manager.get_node_secret(Recipient::PhantomNode);
+ if phantom_secret_res.is_ok() && fake_scid::is_valid_phantom(&self.fake_scid_rand_bytes, short_chan_id) {
+ let phantom_shared_secret = {
+ let mut arr = [0; 32];
+ arr.copy_from_slice(&SharedSecret::new(&onion_packet.public_key.unwrap(), &phantom_secret_res.unwrap())[..]);
+ arr
+ };
+ let next_hop = match onion_utils::decode_next_hop(phantom_shared_secret, &onion_packet.hop_data, onion_packet.hmac, payment_hash) {
+ Ok(res) => res,
+ Err(onion_utils::OnionDecodeErr::Malformed { err_msg, err_code }) => {
+ let sha256_of_onion = Sha256::hash(&onion_packet.hop_data).into_inner();
+ // In this scenario, the phantom would have sent us an
+ // `update_fail_malformed_htlc`, meaning here we encrypt the error as
+ // if it came from us (the second-to-last hop) but contains the sha256
+ // of the onion.
+ fail_forward!(err_msg, err_code, sha256_of_onion.to_vec(), None);
+ },
+ Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code }) => {
+ fail_forward!(err_msg, err_code, Vec::new(), Some(phantom_shared_secret));
+ },
+ };
+ match next_hop {
+ onion_utils::Hop::Receive(hop_data) => {
+ match self.construct_recv_pending_htlc_info(hop_data, incoming_shared_secret, payment_hash, amt_to_forward, outgoing_cltv_value, Some(phantom_shared_secret)) {
+ Ok(info) => phantom_receives.push((prev_short_channel_id, prev_funding_outpoint, vec![(info, prev_htlc_id)])),
+ Err(ReceiveError { err_code, err_data, msg }) => fail_forward!(msg, err_code, err_data, Some(phantom_shared_secret))
+ }
+ },
+ _ => panic!(),
+ }
+ } else {
+ fail_forward!(format!("Unknown short channel id {} for forward HTLC", short_chan_id), 0x4000 | 10, Vec::new(), None);
+ }
+ } else {
+ fail_forward!(format!("Unknown short channel id {} for forward HTLC", short_chan_id), 0x4000 | 10, Vec::new(), None);
+ }
+ },
HTLCForwardInfo::FailHTLC { .. } => {
// Channel went away before we could fail it. This implies
// the channel is now on chain and our counterparty is
outpoint: prev_funding_outpoint,
htlc_id: prev_htlc_id,
incoming_packet_shared_secret: incoming_shared_secret,
+ // Phantom payments are only PendingHTLCRouting::Receive.
+ phantom_shared_secret: None,
});
match chan.get_mut().send_htlc(amt_to_forward, payment_hash, outgoing_cltv_value, htlc_source.clone(), onion_packet, &self.logger) {
Err(e) => {
HTLCForwardInfo::AddHTLC { prev_short_channel_id, prev_htlc_id, forward_info: PendingHTLCInfo {
routing, incoming_shared_secret, payment_hash, amt_to_forward, .. },
prev_funding_outpoint } => {
- let (cltv_expiry, onion_payload) = match routing {
- PendingHTLCRouting::Receive { payment_data, incoming_cltv_expiry } =>
- (incoming_cltv_expiry, OnionPayload::Invoice(payment_data)),
+ let (cltv_expiry, onion_payload, phantom_shared_secret) = match routing {
+ PendingHTLCRouting::Receive { payment_data, incoming_cltv_expiry, phantom_shared_secret } =>
+ (incoming_cltv_expiry, OnionPayload::Invoice(payment_data), phantom_shared_secret),
PendingHTLCRouting::ReceiveKeysend { payment_preimage, incoming_cltv_expiry } =>
- (incoming_cltv_expiry, OnionPayload::Spontaneous(payment_preimage)),
+ (incoming_cltv_expiry, OnionPayload::Spontaneous(payment_preimage), None),
_ => {
panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive");
}
outpoint: prev_funding_outpoint,
htlc_id: prev_htlc_id,
incoming_packet_shared_secret: incoming_shared_secret,
+ phantom_shared_secret,
},
value: amt_to_forward,
cltv_expiry,
outpoint: prev_funding_outpoint,
htlc_id: $htlc.prev_hop.htlc_id,
incoming_packet_shared_secret: $htlc.prev_hop.incoming_packet_shared_secret,
+ phantom_shared_secret,
}), payment_hash,
HTLCFailReason::Reason { failure_code: 0x4000 | 15, data: htlc_msat_height_data }
));
macro_rules! check_total_value {
($payment_data_total_msat: expr, $payment_secret: expr, $payment_preimage: expr) => {{
- let mut total_value = 0;
let mut payment_received_generated = false;
let htlcs = channel_state.claimable_htlcs.entry(payment_hash)
.or_insert(Vec::new());
continue
}
}
- htlcs.push(claimable_htlc);
+ let mut total_value = claimable_htlc.value;
for htlc in htlcs.iter() {
total_value += htlc.value;
match &htlc.onion_payload {
if total_value >= msgs::MAX_VALUE_MSAT || total_value > $payment_data_total_msat {
log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the total value {} ran over expected value {} (or HTLCs were inconsistent)",
log_bytes!(payment_hash.0), total_value, $payment_data_total_msat);
- for htlc in htlcs.iter() {
- fail_htlc!(htlc);
- }
+ fail_htlc!(claimable_htlc);
} else if total_value == $payment_data_total_msat {
+ htlcs.push(claimable_htlc);
new_events.push(events::Event::PaymentReceived {
payment_hash,
purpose: events::PaymentPurpose::InvoicePayment {
// Nothing to do - we haven't reached the total
// payment value yet, wait until we receive more
// MPP parts.
+ htlcs.push(claimable_htlc);
}
payment_received_generated
}}
for (htlc_source, payment_hash, failure_reason) in failed_forwards.drain(..) {
self.fail_htlc_backwards_internal(self.channel_state.lock().unwrap(), htlc_source, &payment_hash, failure_reason);
}
+ self.forward_htlcs(&mut phantom_receives);
for (counterparty_node_id, err) in handle_errors.drain(..) {
let _ = handle_error!(self, err, counterparty_node_id);
pending_events.push(path_failure);
if let Some(ev) = full_failure_ev { pending_events.push(ev); }
},
- HTLCSource::PreviousHopData(HTLCPreviousHopData { short_channel_id, htlc_id, incoming_packet_shared_secret, .. }) => {
+ HTLCSource::PreviousHopData(HTLCPreviousHopData { short_channel_id, htlc_id, incoming_packet_shared_secret, phantom_shared_secret, .. }) => {
let err_packet = match onion_error {
HTLCFailReason::Reason { failure_code, data } => {
log_trace!(self.logger, "Failing HTLC with payment_hash {} backwards from us with code {}", log_bytes!(payment_hash.0), failure_code);
- let packet = onion_utils::build_failure_packet(&incoming_packet_shared_secret, failure_code, &data[..]).encode();
- onion_utils::encrypt_failure_packet(&incoming_packet_shared_secret, &packet)
+ if let Some(phantom_ss) = phantom_shared_secret {
+ let phantom_packet = onion_utils::build_failure_packet(&phantom_ss, failure_code, &data[..]).encode();
+ let encrypted_phantom_packet = onion_utils::encrypt_failure_packet(&phantom_ss, &phantom_packet);
+ onion_utils::encrypt_failure_packet(&incoming_packet_shared_secret, &encrypted_phantom_packet.data[..])
+ } else {
+ let packet = onion_utils::build_failure_packet(&incoming_packet_shared_secret, failure_code, &data[..]).encode();
+ onion_utils::encrypt_failure_packet(&incoming_packet_shared_secret, &packet)
+ }
},
HTLCFailReason::LightningError { err } => {
log_trace!(self.logger, "Failing HTLC with payment_hash {} backwards with pre-built LightningError", log_bytes!(payment_hash.0));
onion_utils::build_first_hop_failure_packet(incoming_shared_secret, error_code, &{
let mut res = Vec::with_capacity(8 + 128);
// TODO: underspecified, follow https://github.com/lightningnetwork/lightning-rfc/issues/791
- res.extend_from_slice(&byte_utils::be16_to_array(0));
+ if error_code == 0x1000 | 20 {
+ res.extend_from_slice(&byte_utils::be16_to_array(0));
+ }
res.extend_from_slice(&upd.encode_with_len()[..]);
res
}[..])
/// In chanmon_consistency_target, we'd like to be able to restore monitor updating without
/// handling all pending events (i.e. not PendingHTLCsForwardable). Thus, we expose monitor
/// update events as a separate process method here.
- #[cfg(feature = "fuzztarget")]
+ #[cfg(fuzzing)]
pub fn process_monitor_events(&self) {
self.process_pending_monitor_events();
}
}
}
- #[cfg(any(test, feature = "fuzztarget", feature = "_test_utils"))]
+ #[cfg(any(test, fuzzing, feature = "_test_utils"))]
pub fn get_and_clear_pending_events(&self) -> Vec<events::Event> {
let events = core::cell::RefCell::new(Vec::new());
let event_handler = |event: &events::Event| events.borrow_mut().push(event.clone());
},
(1, Receive) => {
(0, payment_data, required),
+ (1, phantom_shared_secret, option),
(2, incoming_cltv_expiry, required),
},
(2, ReceiveKeysend) => {
impl_writeable_tlv_based!(HTLCPreviousHopData, {
(0, short_channel_id, required),
+ (1, phantom_shared_secret, option),
(2, outpoint, required),
(4, htlc_id, required),
(6, incoming_packet_shared_secret, required)