next_outbound_htlc_limit_msat: capacity.saturating_mul(1000),
inbound_htlc_minimum_msat: None,
inbound_htlc_maximum_msat: None,
+ config: None,
});
}
Some(&first_hops_vec[..])
use util::ser::{Readable, ReadableArgs, Writeable, Writer, VecWriter};
use util::logger::Logger;
use util::errors::APIError;
-use util::config::{UserConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits};
+use util::config::{UserConfig, ChannelConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits};
use util::scid_utils::scid_from_parts;
use io;
/// transaction (not counting the value of the HTLCs themselves).
pub(crate) const MIN_AFFORDABLE_HTLC_COUNT: usize = 4;
+/// When a [`Channel`] has its [`ChannelConfig`] updated, its existing one is stashed for up to this
+/// number of ticks to allow forwarding HTLCs by nodes that have yet to receive the new
+/// ChannelUpdate prompted by the config update. This value was determined as follows:
+///
+/// * The expected interval between ticks (1 minute).
+/// * The average convergence delay of updates across the network, i.e., ~300 seconds on average
+/// for a node to see an update as seen on `<https://arxiv.org/pdf/2205.12737.pdf>`.
+/// * `EXPIRE_PREV_CONFIG_TICKS` = convergence_delay / tick_interval
+pub(crate) const EXPIRE_PREV_CONFIG_TICKS: usize = 5;
+
// TODO: We should refactor this to be an Inbound/OutboundChannel until initial setup handshaking
// has been completed, and then turn into a Channel to get compiler-time enforcement of things like
// calling channel_id() before we're set up or things like get_outbound_funding_signed on an
// Holder designates channel data owned for the benefice of the user client.
// Counterparty designates channel data owned by the another channel participant entity.
pub(super) struct Channel<Signer: Sign> {
- #[cfg(any(test, feature = "_test_utils"))]
- pub(crate) config: LegacyChannelConfig,
- #[cfg(not(any(test, feature = "_test_utils")))]
config: LegacyChannelConfig,
+ // Track the previous `ChannelConfig` so that we can continue forwarding HTLCs that were
+ // constructed using it. The second element in the tuple corresponds to the number of ticks that
+ // have elapsed since the update occurred.
+ prev_config: Option<(ChannelConfig, usize)>,
+
inbound_handshake_limits_override: Option<ChannelHandshakeLimits>,
user_id: u64,
commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey,
},
+ prev_config: None,
+
inbound_handshake_limits_override: Some(config.channel_handshake_limits.clone()),
channel_id: keys_provider.get_secure_random_bytes(),
commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey,
},
+ prev_config: None,
+
inbound_handshake_limits_override: None,
channel_id: msg.temporary_channel_id,
self.config.options.max_dust_htlc_exposure_msat
}
+ /// Returns the previous [`ChannelConfig`] applied to this channel, if any.
+ pub fn prev_config(&self) -> Option<ChannelConfig> {
+ self.prev_config.map(|prev_config| prev_config.0)
+ }
+
+ /// Tracks the number of ticks elapsed since the previous [`ChannelConfig`] was updated. Once
+ /// [`EXPIRE_PREV_CONFIG_TICKS`] is reached, the previous config is considered expired and will
+ /// no longer be considered when forwarding HTLCs.
+ pub fn maybe_expire_prev_config(&mut self) {
+ if self.prev_config.is_none() {
+ return;
+ }
+ let prev_config = self.prev_config.as_mut().unwrap();
+ prev_config.1 += 1;
+ if prev_config.1 == EXPIRE_PREV_CONFIG_TICKS {
+ self.prev_config = None;
+ }
+ }
+
+ /// Returns the current [`ChannelConfig`] applied to the channel.
+ pub fn config(&self) -> ChannelConfig {
+ self.config.options
+ }
+
+ /// Updates the channel's config. A bool is returned indicating whether the config update
+ /// applied resulted in a new ChannelUpdate message.
+ pub fn update_config(&mut self, config: &ChannelConfig) -> bool {
+ let did_channel_update =
+ self.config.options.forwarding_fee_proportional_millionths != config.forwarding_fee_proportional_millionths ||
+ self.config.options.forwarding_fee_base_msat != config.forwarding_fee_base_msat ||
+ self.config.options.cltv_expiry_delta != config.cltv_expiry_delta;
+ if did_channel_update {
+ self.prev_config = Some((self.config.options, 0));
+ // Update the counter, which backs the ChannelUpdate timestamp, to allow the relay
+ // policy change to propagate throughout the network.
+ self.update_time_counter += 1;
+ }
+ self.config.options = *config;
+ did_channel_update
+ }
+
+ fn internal_htlc_satisfies_config(
+ &self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32, config: &ChannelConfig,
+ ) -> Result<(), (&'static str, u16)> {
+ let fee = amt_to_forward.checked_mul(config.forwarding_fee_proportional_millionths as u64)
+ .and_then(|prop_fee| (prop_fee / 1000000).checked_add(config.forwarding_fee_base_msat as u64));
+ if fee.is_none() || htlc.amount_msat < fee.unwrap() ||
+ (htlc.amount_msat - fee.unwrap()) < amt_to_forward {
+ return Err((
+ "Prior hop has deviated from specified fees parameters or origin node has obsolete ones",
+ 0x1000 | 12, // fee_insufficient
+ ));
+ }
+ if (htlc.cltv_expiry as u64) < outgoing_cltv_value as u64 + config.cltv_expiry_delta as u64 {
+ return Err((
+ "Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta",
+ 0x1000 | 13, // incorrect_cltv_expiry
+ ));
+ }
+ Ok(())
+ }
+
+ /// Determines whether the parameters of an incoming HTLC to be forwarded satisfy the channel's
+ /// [`ChannelConfig`]. This first looks at the channel's current [`ChannelConfig`], and if
+ /// unsuccessful, falls back to the previous one if one exists.
+ pub fn htlc_satisfies_config(
+ &self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32,
+ ) -> Result<(), (&'static str, u16)> {
+ self.internal_htlc_satisfies_config(&htlc, amt_to_forward, outgoing_cltv_value, &self.config())
+ .or_else(|err| {
+ if let Some(prev_config) = self.prev_config() {
+ self.internal_htlc_satisfies_config(htlc, amt_to_forward, outgoing_cltv_value, &prev_config)
+ } else {
+ Err(err)
+ }
+ })
+ }
+
pub fn get_feerate(&self) -> u32 {
self.feerate_per_kw
}
config: config.unwrap(),
+ prev_config: None,
+
// Note that we don't care about serializing handshake limits as we only ever serialize
// channel data after the handshake has completed.
inbound_handshake_limits_override: None,
use ln::msgs::{ChannelMessageHandler, DecodeError, LightningError, MAX_VALUE_MSAT, OptionalField};
use ln::wire::Encode;
use chain::keysinterface::{Sign, KeysInterface, KeysManager, InMemorySigner, Recipient};
-use util::config::UserConfig;
+use util::config::{UserConfig, ChannelConfig};
use util::events::{EventHandler, EventsProvider, MessageSendEvent, MessageSendEventsProvider, ClosureReason};
use util::{byte_utils, events};
use util::scid_utils::fake_scid;
// the HTLC via a full update_fail_htlc/commitment_signed dance before we hit the
// CLTV_CLAIM_BUFFER point (we static assert that it's at least 3 blocks more).
pub const MIN_CLTV_EXPIRY_DELTA: u16 = 6*7;
-pub(super) const CLTV_FAR_FAR_AWAY: u32 = 6 * 24 * 7; //TODO?
+// This should be long enough to allow a payment path drawn across multiple routing hops with substantial
+// `cltv_expiry_delta`. Indeed, the length of those values is the reaction delay offered to a routing node
+// in case of HTLC on-chain settlement. While appearing less competitive, a node operator could decide to
+// scale them up to suit its security policy. At the network-level, we shouldn't constrain them too much,
+// while avoiding to introduce a DoS vector. Further, a low CTLV_FAR_FAR_AWAY could be a source of
+// routing failure for any HTLC sender picking up an LDK node among the first hops.
+pub(super) const CLTV_FAR_FAR_AWAY: u32 = 14 * 24 * 6;
/// Minimum CLTV difference between the current block height and received inbound payments.
/// Invoices generated for payment to us must set their `min_final_cltv_expiry` field to at least
pub inbound_htlc_minimum_msat: Option<u64>,
/// The largest value HTLC (in msat) we currently will accept, for this channel.
pub inbound_htlc_maximum_msat: Option<u64>,
+ /// Set of configurable parameters that affect channel operation.
+ ///
+ /// This field is only `None` for `ChannelDetails` objects serialized prior to LDK 0.0.109.
+ pub config: Option<ChannelConfig>,
}
impl ChannelDetails {
is_usable: channel.is_live(),
is_public: channel.should_announce(),
inbound_htlc_minimum_msat: Some(channel.get_holder_htlc_minimum_msat()),
- inbound_htlc_maximum_msat: channel.get_holder_htlc_maximum_msat()
+ inbound_htlc_maximum_msat: channel.get_holder_htlc_maximum_msat(),
+ config: Some(channel.config()),
});
}
}
},
Some(id) => Some(id.clone()),
};
- let (chan_update_opt, forwardee_cltv_expiry_delta) = if let Some(forwarding_id) = forwarding_id_opt {
+ let chan_update_opt = if let Some(forwarding_id) = forwarding_id_opt {
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
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));
+ if let Err((err, code)) = chan.htlc_satisfies_config(&msg, *amt_to_forward, *outgoing_cltv_value) {
+ break Some((err, code, chan_update_opt));
}
- (chan_update_opt, chan.get_cltv_expiry_delta())
- } else { (None, MIN_CLTV_EXPIRY_DELTA) };
+ chan_update_opt
+ } else {
+ if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + MIN_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, None,
+ ));
+ }
+ None
+ };
- 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
/// Returns an [`APIError::APIMisuseError`] if the funding_transaction spent non-SegWit outputs
/// or if no output was found which matches the parameters in [`Event::FundingGenerationReady`].
///
+ /// Returns [`APIError::APIMisuseError`] if the funding transaction is not final for propagation
+ /// across the p2p network.
+ ///
/// Returns [`APIError::ChannelUnavailable`] if a funding transaction has already been provided
/// for the channel or if the channel has been closed as indicated by [`Event::ChannelClosed`].
///
/// not currently support replacing a funding transaction on an existing channel. Instead,
/// create a new channel with a conflicting funding transaction.
///
+ /// Note to keep the miner incentives aligned in moving the blockchain forward, we recommend
+ /// the wallet software generating the funding transaction to apply anti-fee sniping as
+ /// implemented by Bitcoin Core wallet. See <https://bitcoinops.org/en/topics/fee-sniping/>
+ /// for more details.
+ ///
/// [`Event::FundingGenerationReady`]: crate::util::events::Event::FundingGenerationReady
/// [`Event::ChannelClosed`]: crate::util::events::Event::ChannelClosed
pub fn funding_transaction_generated(&self, temporary_channel_id: &[u8; 32], counterparty_node_id: &PublicKey, funding_transaction: Transaction) -> Result<(), APIError> {
});
}
}
+ {
+ let height = self.best_block.read().unwrap().height();
+ // Transactions are evaluated as final by network mempools at the next block. However, the modules
+ // constituting our Lightning node might not have perfect sync about their blockchain views. Thus, if
+ // the wallet module is in advance on the LDK view, allow one more block of headroom.
+ // TODO: updated if/when https://github.com/rust-bitcoin/rust-bitcoin/pull/994 landed and rust-bitcoin bumped.
+ if !funding_transaction.input.iter().all(|input| input.sequence == 0xffffffff) && funding_transaction.lock_time < 500_000_000 && funding_transaction.lock_time > height + 2 {
+ return Err(APIError::APIMisuseError {
+ err: "Funding transaction absolute timelock is non-final".to_owned()
+ });
+ }
+ }
self.funding_transaction_generated_intern(temporary_channel_id, counterparty_node_id, funding_transaction, |chan, tx| {
let mut output_index = None;
let expected_spk = chan.get_funding_redeemscript().to_v0_p2wsh();
}
}
+ /// Atomically updates the [`ChannelConfig`] for the given channels.
+ ///
+ /// Once the updates are applied, each eligible channel (advertised with a known short channel
+ /// ID and a change in [`forwarding_fee_proportional_millionths`], [`forwarding_fee_base_msat`],
+ /// or [`cltv_expiry_delta`]) has a [`BroadcastChannelUpdate`] event message generated
+ /// containing the new [`ChannelUpdate`] message which should be broadcast to the network.
+ ///
+ /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
+ /// `counterparty_node_id` is provided.
+ ///
+ /// Returns [`APIMisuseError`] when a [`cltv_expiry_delta`] update is to be applied with a value
+ /// below [`MIN_CLTV_EXPIRY_DELTA`].
+ ///
+ /// If an error is returned, none of the updates should be considered applied.
+ ///
+ /// [`forwarding_fee_proportional_millionths`]: ChannelConfig::forwarding_fee_proportional_millionths
+ /// [`forwarding_fee_base_msat`]: ChannelConfig::forwarding_fee_base_msat
+ /// [`cltv_expiry_delta`]: ChannelConfig::cltv_expiry_delta
+ /// [`BroadcastChannelUpdate`]: events::MessageSendEvent::BroadcastChannelUpdate
+ /// [`ChannelUpdate`]: msgs::ChannelUpdate
+ /// [`ChannelUnavailable`]: APIError::ChannelUnavailable
+ /// [`APIMisuseError`]: APIError::APIMisuseError
+ pub fn update_channel_config(
+ &self, counterparty_node_id: &PublicKey, channel_ids: &[[u8; 32]], config: &ChannelConfig,
+ ) -> Result<(), APIError> {
+ if config.cltv_expiry_delta < MIN_CLTV_EXPIRY_DELTA {
+ return Err(APIError::APIMisuseError {
+ err: format!("The chosen CLTV expiry delta is below the minimum of {}", MIN_CLTV_EXPIRY_DELTA),
+ });
+ }
+
+ let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(
+ &self.total_consistency_lock, &self.persistence_notifier,
+ );
+ {
+ let mut channel_state_lock = self.channel_state.lock().unwrap();
+ let channel_state = &mut *channel_state_lock;
+ for channel_id in channel_ids {
+ let channel_counterparty_node_id = channel_state.by_id.get(channel_id)
+ .ok_or(APIError::ChannelUnavailable {
+ err: format!("Channel with ID {} was not found", log_bytes!(*channel_id)),
+ })?
+ .get_counterparty_node_id();
+ if channel_counterparty_node_id != *counterparty_node_id {
+ return Err(APIError::APIMisuseError {
+ err: "counterparty node id mismatch".to_owned(),
+ });
+ }
+ }
+ for channel_id in channel_ids {
+ let channel = channel_state.by_id.get_mut(channel_id).unwrap();
+ if !channel.update_config(config) {
+ continue;
+ }
+ if let Ok(msg) = self.get_channel_update_for_broadcast(channel) {
+ channel_state.pending_msg_events.push(events::MessageSendEvent::BroadcastChannelUpdate { msg });
+ } else if let Ok(msg) = self.get_channel_update_for_unicast(channel) {
+ channel_state.pending_msg_events.push(events::MessageSendEvent::SendChannelUpdate {
+ node_id: channel.get_counterparty_node_id(),
+ msg,
+ });
+ }
+ }
+ }
+ Ok(())
+ }
+
/// Processes HTLCs which are pending waiting on random forward delay.
///
/// Should only really ever be called in response to a PendingHTLCsForwardable event.
/// * Broadcasting `ChannelUpdate` messages if we've been disconnected from our peer for more
/// than a minute, informing the network that they should no longer attempt to route over
/// the channel.
+ /// * Expiring a channel's previous `ChannelConfig` if necessary to only allow forwarding HTLCs
+ /// with the current `ChannelConfig`.
///
/// Note that this may cause reentrancy through `chain::Watch::update_channel` calls or feerate
/// estimate fetches.
_ => {},
}
+ chan.maybe_expire_prev_config();
+
true
});
(4, counterparty, required),
(5, outbound_scid_alias, option),
(6, funding_txo, option),
+ (7, config, option),
(8, short_channel_id, option),
(10, channel_value_satoshis, required),
(12, unspendable_punishment_reserve, option),
($node: expr, $prev_node: expr, $next_node: expr, $new_msgs: expr) => {
{
$node.node.handle_update_fulfill_htlc(&$prev_node.node.get_our_node_id(), &next_msgs.as_ref().unwrap().0);
- let fee = $node.node.channel_state.lock().unwrap()
- .by_id.get(&next_msgs.as_ref().unwrap().0.channel_id).unwrap()
- .config.options.forwarding_fee_base_msat;
+ let fee = {
+ let channel_state = $node.node.channel_state.lock().unwrap();
+ let channel = channel_state
+ .by_id.get(&next_msgs.as_ref().unwrap().0.channel_id).unwrap();
+ if let Some(prev_config) = channel.prev_config() {
+ prev_config.forwarding_fee_base_msat
+ } else {
+ channel.config().forwarding_fee_base_msat
+ }
+ };
expect_payment_forwarded!($node, $next_node, $prev_node, Some(fee as u64), false, false);
expected_total_fee_msat += fee as u64;
check_added_monitors!($node, 1);
use bitcoin::blockdata::opcodes;
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::network::constants::Network;
+use bitcoin::{Transaction, TxIn, TxOut, Witness};
+use bitcoin::OutPoint as BitcoinOutPoint;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::secp256k1::{PublicKey,SecretKey};
do_test_max_dust_htlc_exposure(false, ExposureEvent::AtUpdateFeeOutbound, false);
do_test_max_dust_htlc_exposure(false, ExposureEvent::AtUpdateFeeOutbound, true);
}
+
+#[test]
+fn test_non_final_funding_tx() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let temp_channel_id = nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 42, None).unwrap();
+ let open_channel_message = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
+ nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), InitFeatures::known(), &open_channel_message);
+ let accept_channel_message = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id());
+ nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), InitFeatures::known(), &accept_channel_message);
+
+ let best_height = nodes[0].node.best_block.read().unwrap().height();
+
+ let chan_id = *nodes[0].network_chan_count.borrow();
+ let events = nodes[0].node.get_and_clear_pending_events();
+ let input = TxIn { previous_output: BitcoinOutPoint::null(), script_sig: bitcoin::Script::new(), sequence: 0x1, witness: Witness::from_vec(vec!(vec!(1))) };
+ assert_eq!(events.len(), 1);
+ let mut tx = match events[0] {
+ Event::FundingGenerationReady { ref channel_value_satoshis, ref output_script, .. } => {
+ // Timelock the transaction _beyond_ the best client height + 2.
+ Transaction { version: chan_id as i32, lock_time: best_height + 3, input: vec![input], output: vec![TxOut {
+ value: *channel_value_satoshis, script_pubkey: output_script.clone(),
+ }]}
+ },
+ _ => panic!("Unexpected event"),
+ };
+ // Transaction should fail as it's evaluated as non-final for propagation.
+ match nodes[0].node.funding_transaction_generated(&temp_channel_id, &nodes[1].node.get_our_node_id(), tx.clone()) {
+ Err(APIError::APIMisuseError { err }) => {
+ assert_eq!(format!("Funding transaction absolute timelock is non-final"), err);
+ },
+ _ => panic!()
+ }
+
+ // However, transaction should be accepted if it's in a +2 headroom from best block.
+ tx.lock_time -= 1;
+ assert!(nodes[0].node.funding_transaction_generated(&temp_channel_id, &nodes[1].node.get_our_node_id(), tx.clone()).is_ok());
+ get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, nodes[1].node.get_our_node_id());
+}
//! These tests work by standing up full nodes and route payments across the network, checking the
//! returned errors decode to the correct thing.
-use chain::channelmonitor::{CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
+use chain::channelmonitor::{ChannelMonitor, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
use chain::keysinterface::{KeysInterface, Recipient};
use ln::{PaymentHash, PaymentSecret};
-use ln::channelmanager::{HTLCForwardInfo, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, PendingHTLCInfo, PendingHTLCRouting};
+use ln::channel::EXPIRE_PREV_CONFIG_TICKS;
+use ln::channelmanager::{ChannelManager, ChannelManagerReadArgs, HTLCForwardInfo, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, PendingHTLCInfo, PendingHTLCRouting};
use ln::onion_utils;
use routing::gossip::{NetworkUpdate, RoutingFees, NodeId};
use routing::router::{get_route, PaymentParameters, Route, RouteHint, RouteHintHop};
use ln::msgs::{ChannelMessageHandler, ChannelUpdate, OptionalField};
use ln::wire::Encode;
use util::events::{Event, MessageSendEvent, MessageSendEventsProvider};
-use util::ser::{Writeable, Writer};
+use util::ser::{ReadableArgs, Writeable, Writer};
use util::{byte_utils, test_utils};
-use util::config::UserConfig;
+use util::config::{UserConfig, ChannelConfig};
+use util::errors::APIError;
use bitcoin::hash_types::BlockHash;
let preimage = send_along_route(&nodes[0], bogus_route, &[&nodes[1], &nodes[2]], amt_to_forward+1).0;
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], preimage);
- //TODO: with new config API, we will be able to generate both valid and
- //invalid channel_update cases.
let short_channel_id = channels[0].0.contents.short_channel_id;
run_onion_failure_test("fee_insufficient", 0, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, true, Some(23), None, None);
}
+fn do_test_onion_failure_stale_channel_update(announced_channel: bool) {
+ // Create a network of three nodes and two channels connecting them. We'll be updating the
+ // HTLC relay policy of the second channel, causing forwarding failures at the first hop.
+ let mut config = UserConfig::default();
+ config.channel_handshake_config.announced_channel = announced_channel;
+ config.channel_handshake_limits.force_announced_channel_preference = false;
+ config.accept_forwards_to_priv_channels = !announced_channel;
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(config), None]);
+ let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs);
+
+ let other_channel = create_chan_between_nodes(
+ &nodes[0], &nodes[1], InitFeatures::known(), InitFeatures::known(),
+ );
+ let channel_to_update = if announced_channel {
+ let channel = create_announced_chan_between_nodes(
+ &nodes, 1, 2, InitFeatures::known(), InitFeatures::known(),
+ );
+ (channel.2, channel.0.contents.short_channel_id)
+ } else {
+ let channel = create_unannounced_chan_between_nodes_with_value(
+ &nodes, 1, 2, 100000, 10001, InitFeatures::known(), InitFeatures::known(),
+ );
+ (channel.0.channel_id, channel.0.short_channel_id_alias.unwrap())
+ };
+ let channel_to_update_counterparty = &nodes[2].node.get_our_node_id();
+
+ let default_config = ChannelConfig::default();
+
+ // A test payment should succeed as the ChannelConfig has not been changed yet.
+ const PAYMENT_AMT: u64 = 40000;
+ let (route, payment_hash, payment_preimage, payment_secret) = if announced_channel {
+ get_route_and_payment_hash!(nodes[0], nodes[2], PAYMENT_AMT)
+ } else {
+ let hop_hints = vec![RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[1].node.get_our_node_id(),
+ short_channel_id: channel_to_update.1,
+ fees: RoutingFees {
+ base_msat: default_config.forwarding_fee_base_msat,
+ proportional_millionths: default_config.forwarding_fee_proportional_millionths,
+ },
+ cltv_expiry_delta: default_config.cltv_expiry_delta,
+ htlc_maximum_msat: None,
+ htlc_minimum_msat: None,
+ }])];
+ let payment_params = PaymentParameters::from_node_id(*channel_to_update_counterparty)
+ .with_features(InvoiceFeatures::known())
+ .with_route_hints(hop_hints);
+ get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, PAYMENT_AMT, TEST_FINAL_CLTV)
+ };
+ send_along_route_with_secret(&nodes[0], route.clone(), &[&[&nodes[1], &nodes[2]]], PAYMENT_AMT,
+ payment_hash, payment_secret);
+ claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
+
+ // Closure to force expiry of a channel's previous config.
+ let expire_prev_config = || {
+ for _ in 0..EXPIRE_PREV_CONFIG_TICKS {
+ nodes[1].node.timer_tick_occurred();
+ }
+ };
+
+ // Closure to update and retrieve the latest ChannelUpdate.
+ let update_and_get_channel_update = |config: &ChannelConfig, expect_new_update: bool,
+ prev_update: Option<&msgs::ChannelUpdate>, should_expire_prev_config: bool| -> Option<msgs::ChannelUpdate> {
+ nodes[1].node.update_channel_config(
+ channel_to_update_counterparty, &[channel_to_update.0], config,
+ ).unwrap();
+ let events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), expect_new_update as usize);
+ if !expect_new_update {
+ return None;
+ }
+ let new_update = match &events[0] {
+ MessageSendEvent::BroadcastChannelUpdate { msg } => {
+ assert!(announced_channel);
+ msg.clone()
+ },
+ MessageSendEvent::SendChannelUpdate { node_id, msg } => {
+ assert_eq!(node_id, channel_to_update_counterparty);
+ assert!(!announced_channel);
+ msg.clone()
+ },
+ _ => panic!("expected Broadcast/SendChannelUpdate event"),
+ };
+ if prev_update.is_some() {
+ assert!(new_update.contents.timestamp > prev_update.unwrap().contents.timestamp)
+ }
+ if should_expire_prev_config {
+ expire_prev_config();
+ }
+ Some(new_update)
+ };
+
+ // We'll be attempting to route payments using the default ChannelUpdate for channels. This will
+ // lead to onion failures at the first hop once we update the ChannelConfig for the
+ // second hop.
+ let expect_onion_failure = |name: &str, error_code: u16, channel_update: &msgs::ChannelUpdate| {
+ let short_channel_id = channel_to_update.1;
+ let network_update = NetworkUpdate::ChannelUpdateMessage { msg: channel_update.clone() };
+ run_onion_failure_test(
+ name, 0, &nodes, &route, &payment_hash, &payment_secret, |_| {}, || {}, true,
+ Some(error_code), Some(network_update), Some(short_channel_id),
+ );
+ };
+
+ // Updates to cltv_expiry_delta below MIN_CLTV_EXPIRY_DELTA should fail with APIMisuseError.
+ let mut invalid_config = default_config.clone();
+ invalid_config.cltv_expiry_delta = 0;
+ match nodes[1].node.update_channel_config(
+ channel_to_update_counterparty, &[channel_to_update.0], &invalid_config,
+ ) {
+ Err(APIError::APIMisuseError{ .. }) => {},
+ _ => panic!("unexpected result applying invalid cltv_expiry_delta"),
+ }
+
+ // Increase the base fee which should trigger a new ChannelUpdate.
+ let mut config = nodes[1].node.list_usable_channels().iter()
+ .find(|channel| channel.channel_id == channel_to_update.0).unwrap()
+ .config.unwrap();
+ config.forwarding_fee_base_msat = u32::max_value();
+ let msg = update_and_get_channel_update(&config, true, None, false).unwrap();
+
+ // The old policy should still be in effect until a new block is connected.
+ send_along_route_with_secret(&nodes[0], route.clone(), &[&[&nodes[1], &nodes[2]]], PAYMENT_AMT,
+ payment_hash, payment_secret);
+ claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
+
+ // Connect a block, which should expire the previous config, leading to a failure when
+ // forwarding the HTLC.
+ expire_prev_config();
+ expect_onion_failure("fee_insufficient", UPDATE|12, &msg);
+
+ // Redundant updates should not trigger a new ChannelUpdate.
+ assert!(update_and_get_channel_update(&config, false, None, false).is_none());
+
+ // Similarly, updates that do not have an affect on ChannelUpdate should not trigger a new one.
+ config.force_close_avoidance_max_fee_satoshis *= 2;
+ assert!(update_and_get_channel_update(&config, false, None, false).is_none());
+
+ // Reset the base fee to the default and increase the proportional fee which should trigger a
+ // new ChannelUpdate.
+ config.forwarding_fee_base_msat = default_config.forwarding_fee_base_msat;
+ config.cltv_expiry_delta = u16::max_value();
+ let msg = update_and_get_channel_update(&config, true, Some(&msg), true).unwrap();
+ expect_onion_failure("incorrect_cltv_expiry", UPDATE|13, &msg);
+
+ // Reset the proportional fee and increase the CLTV expiry delta which should trigger a new
+ // ChannelUpdate.
+ config.cltv_expiry_delta = default_config.cltv_expiry_delta;
+ config.forwarding_fee_proportional_millionths = u32::max_value();
+ let msg = update_and_get_channel_update(&config, true, Some(&msg), true).unwrap();
+ expect_onion_failure("fee_insufficient", UPDATE|12, &msg);
+
+ // To test persistence of the updated config, we'll re-initialize the ChannelManager.
+ let config_after_restart = {
+ let persister = test_utils::TestPersister::new();
+ let chain_monitor = test_utils::TestChainMonitor::new(
+ Some(nodes[1].chain_source), nodes[1].tx_broadcaster.clone(), nodes[1].logger,
+ node_cfgs[1].fee_estimator, &persister, nodes[1].keys_manager,
+ );
+
+ let mut chanmon_1 = <(_, ChannelMonitor<_>)>::read(
+ &mut &get_monitor!(nodes[1], other_channel.3).encode()[..], nodes[1].keys_manager,
+ ).unwrap().1;
+ let mut chanmon_2 = <(_, ChannelMonitor<_>)>::read(
+ &mut &get_monitor!(nodes[1], channel_to_update.0).encode()[..], nodes[1].keys_manager,
+ ).unwrap().1;
+ let mut channel_monitors = HashMap::new();
+ channel_monitors.insert(chanmon_1.get_funding_txo().0, &mut chanmon_1);
+ channel_monitors.insert(chanmon_2.get_funding_txo().0, &mut chanmon_2);
+
+ let chanmgr = <(_, ChannelManager<_, _, _, _, _, _>)>::read(
+ &mut &nodes[1].node.encode()[..], ChannelManagerReadArgs {
+ default_config: *nodes[1].node.get_current_default_configuration(),
+ keys_manager: nodes[1].keys_manager,
+ fee_estimator: node_cfgs[1].fee_estimator,
+ chain_monitor: &chain_monitor,
+ tx_broadcaster: nodes[1].tx_broadcaster.clone(),
+ logger: nodes[1].logger,
+ channel_monitors: channel_monitors,
+ },
+ ).unwrap().1;
+ chanmgr.list_channels().iter()
+ .find(|channel| channel.channel_id == channel_to_update.0).unwrap()
+ .config.unwrap()
+ };
+ assert_eq!(config, config_after_restart);
+}
+
+#[test]
+fn test_onion_failure_stale_channel_update() {
+ do_test_onion_failure_stale_channel_update(false);
+ do_test_onion_failure_stale_channel_update(true);
+}
+
#[test]
fn test_default_to_onion_payload_tlv_format() {
// Tests that we default to creating tlv format onion payloads when no `NodeAnnouncementInfo`
use chain::channelmonitor::{ANTI_REORG_DELAY, ChannelMonitor, LATENCY_GRACE_PERIOD_BLOCKS};
use chain::transaction::OutPoint;
use chain::keysinterface::KeysInterface;
+use ln::channel::EXPIRE_PREV_CONFIG_TICKS;
use ln::channelmanager::{BREAKDOWN_TIMEOUT, ChannelManager, ChannelManagerReadArgs, MPP_TIMEOUT_TICKS, PaymentId, PaymentSendFailure};
use ln::features::{InitFeatures, InvoiceFeatures};
use ln::msgs;
// Update the fee on the middle hop to ensure PaymentSent events have the correct (retried) fee
// and not the original fee. We also update node[1]'s relevant config as
// do_claim_payment_along_route expects us to never overpay.
- nodes[1].node.channel_state.lock().unwrap().by_id.get_mut(&chan_id_2).unwrap()
- .config.options.forwarding_fee_base_msat += 100_000;
- new_route.paths[0][0].fee_msat += 100_000;
+ {
+ let mut channel_state = nodes[1].node.channel_state.lock().unwrap();
+ let mut channel = channel_state.by_id.get_mut(&chan_id_2).unwrap();
+ let mut new_config = channel.config();
+ new_config.forwarding_fee_base_msat += 100_000;
+ channel.update_config(&new_config);
+ new_route.paths[0][0].fee_msat += 100_000;
+ }
+
+ // Force expiration of the channel's previous config.
+ for _ in 0..EXPIRE_PREV_CONFIG_TICKS {
+ nodes[1].node.timer_tick_occurred();
+ }
assert!(nodes[0].node.retry_payment(&new_route, payment_id_1).is_err()); // Shouldn't be allowed to retry a fulfilled payment
nodes[0].node.retry_payment(&new_route, payment_id).unwrap();
/// Moniker assigned to the node.
/// May be invalid or malicious (eg control chars),
/// should not be exposed to the user.
- pub alias: [u8; 32],
+ pub alias: NodeAlias,
/// Internet-level addresses via which one can connect to the node
pub addresses: Vec<NetAddress>,
/// An initial announcement of the node
(10, addresses, vec_type),
});
+/// A user-defined name for a node, which may be used when displaying the node in a graph.
+///
+/// Since node aliases are provided by third parties, they are a potential avenue for injection
+/// attacks. Care must be taken when processing.
+#[derive(Clone, Debug, PartialEq)]
+pub struct NodeAlias(pub [u8; 32]);
+
+impl fmt::Display for NodeAlias {
+ fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ let control_symbol = core::char::REPLACEMENT_CHARACTER;
+ let first_null = self.0.iter().position(|b| *b == 0).unwrap_or(self.0.len());
+ let bytes = self.0.split_at(first_null).0;
+ match core::str::from_utf8(bytes) {
+ Ok(alias) => {
+ for c in alias.chars() {
+ let mut bytes = [0u8; 4];
+ let c = if !c.is_control() { c } else { control_symbol };
+ f.write_str(c.encode_utf8(&mut bytes))?;
+ }
+ },
+ Err(_) => {
+ for c in bytes.iter().map(|b| *b as char) {
+ // Display printable ASCII characters
+ let mut bytes = [0u8; 4];
+ let c = if c >= '\x20' && c <= '\x7e' { c } else { control_symbol };
+ f.write_str(c.encode_utf8(&mut bytes))?;
+ }
+ },
+ };
+ Ok(())
+ }
+}
+
+impl Writeable for NodeAlias {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.0.write(w)
+ }
+}
+
+impl Readable for NodeAlias {
+ fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
+ Ok(NodeAlias(Readable::read(r)?))
+ }
+}
+
#[derive(Clone, Debug, PartialEq)]
/// Details about a node in the network, known from the network announcement.
pub struct NodeInfo {
features: msg.features.clone(),
last_update: msg.timestamp,
rgb: msg.rgb,
- alias: msg.alias,
+ alias: NodeAlias(msg.alias),
addresses: msg.addresses.clone(),
announcement_message: if should_relay { full_msg.cloned() } else { None },
});
use chain;
use ln::PaymentHash;
use ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
- use routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate, MAX_EXCESS_BYTES_FOR_RELAY};
+ use routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate, NodeAlias, MAX_EXCESS_BYTES_FOR_RELAY};
use ln::msgs::{Init, OptionalField, RoutingMessageHandler, UnsignedNodeAnnouncement, NodeAnnouncement,
UnsignedChannelAnnouncement, ChannelAnnouncement, UnsignedChannelUpdate, ChannelUpdate,
ReplyChannelRange, QueryChannelRange, QueryShortChannelIds, MAX_VALUE_MSAT};
});
assert!(result.is_err());
}
+
+ #[test]
+ fn displays_node_alias() {
+ let format_str_alias = |alias: &str| {
+ let mut bytes = [0u8; 32];
+ bytes[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes());
+ format!("{}", NodeAlias(bytes))
+ };
+
+ assert_eq!(format_str_alias("I\u{1F496}LDK! \u{26A1}"), "I\u{1F496}LDK! \u{26A1}");
+ assert_eq!(format_str_alias("I\u{1F496}LDK!\0\u{26A1}"), "I\u{1F496}LDK!");
+ assert_eq!(format_str_alias("I\u{1F496}LDK!\t\u{26A1}"), "I\u{1F496}LDK!\u{FFFD}\u{26A1}");
+
+ let format_bytes_alias = |alias: &[u8]| {
+ let mut bytes = [0u8; 32];
+ bytes[..alias.len()].copy_from_slice(alias);
+ format!("{}", NodeAlias(bytes))
+ };
+
+ assert_eq!(format_bytes_alias(b"\xFFI <heart> LDK!"), "\u{FFFD}I <heart> LDK!");
+ assert_eq!(format_bytes_alias(b"\xFFI <heart>\0LDK!"), "\u{FFFD}I <heart>");
+ assert_eq!(format_bytes_alias(b"\xFFI <heart>\tLDK!"), "\u{FFFD}I <heart>\u{FFFD}LDK!");
+ }
}
#[cfg(all(test, feature = "_bench_unstable"))]
is_usable: true, is_public: true,
inbound_htlc_minimum_msat: None,
inbound_htlc_maximum_msat: None,
+ config: None,
}
}
is_public: true,
inbound_htlc_minimum_msat: None,
inbound_htlc_maximum_msat: None,
+ config: None,
}
}
}
}
}
+
+ /// Query the estimated minimum and maximum liquidity available for sending a payment over the
+ /// channel with `scid` towards the given `target` node.
+ pub fn estimated_channel_liquidity_range(&self, scid: u64, target: &NodeId) -> Option<(u64, u64)> {
+ let graph = self.network_graph.read_only();
+
+ if let Some(chan) = graph.channels().get(&scid) {
+ if let Some(liq) = self.channel_liquidities.get(&scid) {
+ if let Some((directed_info, source)) = chan.as_directed_to(target) {
+ let amt = directed_info.effective_capacity().as_msat();
+ let dir_liq = liq.as_directed(source, target, amt, self.params.liquidity_offset_half_life);
+ return Some((dir_liq.min_liquidity_msat(), dir_liq.max_liquidity_msat()));
+ }
+ }
+ }
+ None
+ }
}
impl ProbabilisticScoringParameters {
/// Options which apply on a per-channel basis and may change at runtime or based on negotiation
/// with our counterparty.
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ChannelConfig {
/// Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound
/// over the channel.
}
}
+impl_writeable_tlv_based!(ChannelConfig, {
+ (0, forwarding_fee_proportional_millionths, required),
+ (2, forwarding_fee_base_msat, required),
+ (4, cltv_expiry_delta, required),
+ (6, max_dust_htlc_exposure_msat, required),
+ // ChannelConfig serialized this field with a required type of 8 prior to the introduction of
+ // LegacyChannelConfig. To make sure that serialization is not compatible with this one, we use
+ // the next required type of 10, which if seen by the old serialization will always fail.
+ (10, force_close_avoidance_max_fee_satoshis, required),
+});
+
/// Legacy version of [`ChannelConfig`] that stored the static
/// [`ChannelHandshakeConfig::announced_channel`] and
/// [`ChannelHandshakeConfig::commit_upfront_shutdown_pubkey`] fields.