err: msg,
action: msgs::ErrorAction::IgnoreError,
},
- ChannelError::Close(msg) => LightningError {
+ ChannelError::Close((msg, _reason)) => LightningError {
err: msg.clone(),
action: msgs::ErrorAction::SendErrorMessage {
msg: msgs::ErrorMessage {
/// #
/// # fn example<T: AChannelManager>(channel_manager: T) -> Result<(), Bolt12SemanticError> {
/// # let channel_manager = channel_manager.get_cm();
+/// # let absolute_expiry = None;
/// let offer = channel_manager
-/// .create_offer_builder()?
+/// .create_offer_builder(absolute_expiry)?
/// # ;
/// # // Needed for compiling for c_bindings
/// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into();
/// many peers we reject new (inbound) connections.
const MAX_NO_CHANNEL_PEERS: usize = 250;
+/// The maximum expiration from the current time where an [`Offer`] or [`Refund`] is considered
+/// short-lived, while anything with a greater expiration is considered long-lived.
+///
+/// Using [`ChannelManager::create_offer_builder`] or [`ChannelManager::create_refund_builder`],
+/// will included a [`BlindedPath`] created using:
+/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and
+/// - [`MessageRouter::create_blinded_paths`] when long-lived.
+///
+/// Using compact [`BlindedPath`]s may provide better privacy as the [`MessageRouter`] could select
+/// more hops. However, since they use short channel ids instead of pubkeys, they are more likely to
+/// become invalid over time as channels are closed. Thus, they are only suitable for short-term use.
+pub const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24);
+
/// Used by [`ChannelManager::list_recent_payments`] to express the status of recent payments.
/// These include payments that have yet to find a successful path, or have unresolved HTLCs.
#[derive(Debug, PartialEq)]
ChannelError::Ignore(msg) => {
(false, MsgHandleErrInternal::from_chan_no_close(ChannelError::Ignore(msg), *$channel_id))
},
- ChannelError::Close(msg) => {
+ ChannelError::Close((msg, reason)) => {
let logger = WithChannelContext::from(&$self.logger, &$channel.context, None);
log_error!(logger, "Closing channel {} due to close-required error: {}", $channel_id, msg);
update_maps_on_chan_removal!($self, $channel.context);
- let reason = ClosureReason::ProcessingError { err: msg.clone() };
let shutdown_res = $channel.context.force_shutdown(true, reason);
let err =
MsgHandleErrInternal::from_finish_shutdown(msg, *$channel_id, shutdown_res, $channel_update);
Some(ChannelPhase::UnfundedOutboundV1(mut chan)) => {
macro_rules! close_chan { ($err: expr, $api_err: expr, $chan: expr) => { {
let counterparty;
- let err = if let ChannelError::Close(msg) = $err {
+ let err = if let ChannelError::Close((msg, reason)) = $err {
let channel_id = $chan.context.channel_id();
counterparty = chan.context.get_counterparty_node_id();
- let reason = ClosureReason::ProcessingError { err: msg.clone() };
let shutdown_res = $chan.context.force_shutdown(false, reason);
MsgHandleErrInternal::from_finish_shutdown(msg, channel_id, shutdown_res, None)
} else { unreachable!(); };
match find_funding_output(&chan, &funding_transaction) {
Ok(found_funding_txo) => funding_txo = found_funding_txo,
Err(err) => {
- let chan_err = ChannelError::Close(err.to_owned());
+ let chan_err = ChannelError::close(err.to_owned());
let api_err = APIError::APIMisuseError { err: err.to_owned() };
return close_chan!(chan_err, api_err, chan);
},
},
Some(mut phase) => {
let err_msg = format!("Got an unexpected funding_created message from peer with counterparty_node_id {}", counterparty_node_id);
- let err = ChannelError::Close(err_msg);
+ let err = ChannelError::close(err_msg);
return Err(convert_chan_phase_err!(self, err, &mut phase, &msg.temporary_channel_id).1);
},
None => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.temporary_channel_id))
// `update_maps_on_chan_removal`), we'll remove the existing channel
// from `outpoint_to_peer`. Thus, we must first unset the funding outpoint
// on the channel.
- let err = ChannelError::Close($err.to_owned());
+ let err = ChannelError::close($err.to_owned());
chan.unset_funding_info(msg.temporary_channel_id);
return Err(convert_chan_phase_err!(self, err, chan, &funded_channel_id, UNFUNDED_CHANNEL).1);
} } }
} else { unreachable!(); }
Ok(())
} else {
- let e = ChannelError::Close("Channel funding outpoint was a duplicate".to_owned());
+ let e = ChannelError::close("Channel funding outpoint was a duplicate".to_owned());
// We weren't able to watch the channel to begin with, so no
// updates should be made on it. Previously, full_stack_target
// found an (unreachable) panic when the monitor update contained
Ok(())
} else {
- try_chan_phase_entry!(self, Err(ChannelError::Close(
+ try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a channel_ready message for an unfunded channel!".into())), chan_phase_entry)
}
},
(tx, Some(remove_channel_phase!(self, chan_phase_entry)), shutdown_result)
} else { (tx, None, shutdown_result) }
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a closing_signed message for an unfunded channel!".into())), chan_phase_entry);
}
},
}
try_chan_phase_entry!(self, chan.update_add_htlc(&msg, pending_forward_info, &self.fee_estimator), chan_phase_entry);
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an update_add_htlc message for an unfunded channel!".into())), chan_phase_entry);
}
},
next_user_channel_id = chan.context.get_user_id();
res
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an update_fulfill_htlc message for an unfunded channel!".into())), chan_phase_entry);
}
},
if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() {
try_chan_phase_entry!(self, chan.update_fail_htlc(&msg, HTLCFailReason::from_msg(msg)), chan_phase_entry);
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an update_fail_htlc message for an unfunded channel!".into())), chan_phase_entry);
}
},
match peer_state.channel_by_id.entry(msg.channel_id) {
hash_map::Entry::Occupied(mut chan_phase_entry) => {
if (msg.failure_code & 0x8000) == 0 {
- let chan_err: ChannelError = ChannelError::Close("Got update_fail_malformed_htlc with BADONION not set".to_owned());
+ let chan_err = ChannelError::close("Got update_fail_malformed_htlc with BADONION not set".to_owned());
try_chan_phase_entry!(self, Err(chan_err), chan_phase_entry);
}
if let ChannelPhase::Funded(chan) = chan_phase_entry.get_mut() {
try_chan_phase_entry!(self, chan.update_fail_malformed_htlc(&msg, HTLCFailReason::reason(msg.failure_code, msg.sha256_of_onion.to_vec())), chan_phase_entry);
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an update_fail_malformed_htlc message for an unfunded channel!".into())), chan_phase_entry);
}
Ok(())
}
Ok(())
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a commitment_signed message for an unfunded channel!".into())), chan_phase_entry);
}
},
}
htlcs_to_fail
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a revoke_and_ack message for an unfunded channel!".into())), chan_phase_entry);
}
},
let logger = WithChannelContext::from(&self.logger, &chan.context, None);
try_chan_phase_entry!(self, chan.update_fee(&self.fee_estimator, &msg, &&logger), chan_phase_entry);
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an update_fee message for an unfunded channel!".into())), chan_phase_entry);
}
},
update_msg: Some(self.get_channel_update_for_broadcast(chan).unwrap()),
});
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got an announcement_signatures message for an unfunded channel!".into())), chan_phase_entry);
}
},
}
}
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a channel_update for an unfunded channel!".into())), chan_phase_entry);
}
},
}
need_lnd_workaround
} else {
- return try_chan_phase_entry!(self, Err(ChannelError::Close(
+ return try_chan_phase_entry!(self, Err(ChannelError::close(
"Got a channel_reestablish message for an unfunded channel!".into())), chan_phase_entry);
}
},
macro_rules! create_offer_builder { ($self: ident, $builder: ty) => {
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the
- /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer will
- /// not have an expiration unless otherwise set on the builder.
+ /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer's
+ /// expiration will be `absolute_expiry` if `Some`, otherwise it will not expire.
///
/// # Privacy
///
- /// Uses [`MessageRouter::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the
- /// offer. However, if one is not found, uses a one-hop [`BlindedPath`] with
- /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case,
- /// the node must be announced, otherwise, there is no way to find a path to the introduction in
- /// order to send the [`InvoiceRequest`].
+ /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the offer based on the given
+ /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for
+ /// privacy implications as well as those of the parameterized [`Router`], which implements
+ /// [`MessageRouter`].
///
/// Also, uses a derived signing pubkey in the offer for recipient privacy.
///
///
/// [`Offer`]: crate::offers::offer::Offer
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
- pub fn create_offer_builder(&$self) -> Result<$builder, Bolt12SemanticError> {
+ pub fn create_offer_builder(
+ &$self, absolute_expiry: Option<Duration>
+ ) -> Result<$builder, Bolt12SemanticError> {
let node_id = $self.get_our_node_id();
let expanded_key = &$self.inbound_payment_key;
let entropy = &*$self.entropy_source;
let secp_ctx = &$self.secp_ctx;
- let path = $self.create_compact_blinded_path()
+ let path = $self.create_blinded_path_using_absolute_expiry(absolute_expiry)
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
let builder = OfferBuilder::deriving_signing_pubkey(
node_id, expanded_key, entropy, secp_ctx
.chain_hash($self.chain_hash)
.path(path);
+ let builder = match absolute_expiry {
+ None => builder,
+ Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry),
+ };
+
Ok(builder.into())
}
} }
///
/// # Privacy
///
- /// Uses [`MessageRouter::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the
- /// refund. However, if one is not found, uses a one-hop [`BlindedPath`] with
- /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case,
- /// the node must be announced, otherwise, there is no way to find a path to the introduction in
- /// order to send the [`Bolt12Invoice`].
+ /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the refund based on the given
+ /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for
+ /// privacy implications as well as those of the parameterized [`Router`], which implements
+ /// [`MessageRouter`].
///
/// Also, uses a derived payer id in the refund for payer privacy.
///
let entropy = &*$self.entropy_source;
let secp_ctx = &$self.secp_ctx;
- let path = $self.create_compact_blinded_path()
+ let path = $self.create_blinded_path_using_absolute_expiry(Some(absolute_expiry))
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
let builder = RefundBuilder::deriving_payer_id(
node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id
///
/// # Privacy
///
- /// Uses a one-hop [`BlindedPath`] for the reply path with [`ChannelManager::get_our_node_id`]
- /// as the introduction node and a derived payer id for payer privacy. As such, currently, the
- /// node must be announced. Otherwise, there is no way to find a path to the introduction node
- /// in order to send the [`Bolt12Invoice`].
+ /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`]
+ /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the
+ /// docs of the parameterized [`Router`], which implements [`MessageRouter`].
///
/// # Limitations
///
inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key)
}
+ /// Creates a blinded path by delegating to [`MessageRouter`] based on the path's intended
+ /// lifetime.
+ ///
+ /// Whether or not the path is compact depends on whether the path is short-lived or long-lived,
+ /// respectively, based on the given `absolute_expiry` as seconds since the Unix epoch. See
+ /// [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`].
+ fn create_blinded_path_using_absolute_expiry(
+ &self, absolute_expiry: Option<Duration>
+ ) -> Result<BlindedPath, ()> {
+ let now = self.duration_since_epoch();
+ let max_short_lived_absolute_expiry = now.saturating_add(MAX_SHORT_LIVED_RELATIVE_EXPIRY);
+
+ if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry {
+ self.create_compact_blinded_path()
+ } else {
+ self.create_blinded_path()
+ }
+ }
+
+ pub(super) fn duration_since_epoch(&self) -> Duration {
+ #[cfg(not(feature = "std"))]
+ let now = Duration::from_secs(
+ self.highest_seen_timestamp.load(Ordering::Acquire) as u64
+ );
+ #[cfg(feature = "std")]
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)
+ .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
+
+ now
+ }
+
/// Creates a blinded path by delegating to [`MessageRouter::create_blinded_paths`].
///
/// Errors if the `MessageRouter` errors or returns an empty `Vec`.
let peers = self.per_peer_state.read().unwrap()
.iter()
- .filter(|(_, peer)| peer.lock().unwrap().latest_features.supports_onion_messages())
+ .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap()))
+ .filter(|(_, peer)| peer.is_connected)
+ .filter(|(_, peer)| peer.latest_features.supports_onion_messages())
.map(|(node_id, _)| *node_id)
.collect::<Vec<_>>();
let peers = self.per_peer_state.read().unwrap()
.iter()
.map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap()))
+ .filter(|(_, peer)| peer.is_connected)
.filter(|(_, peer)| peer.latest_features.supports_onion_messages())
.map(|(node_id, peer)| ForwardNode {
node_id: *node_id,