Skip to content

Make the base fee configurable in ChannelConfig #975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 9, 2021

Conversation

TheBlueMatt
Copy link
Collaborator

Currently the base fee we apply is always the expected cost to
claim an HTLC on-chain in case of closure. This results in
significantly higher than market rate fees [1], and doesn't really
match the actual forwarding trust model anyway - as long as
channel counterparties are honest, our HTLCs shouldn't end up
on-chain no matter what the HTLC sender/recipient do.

While some users may wish to use a feerate that implies they will
not lose funds even if they go to chain (assuming no flood-and-loot
style attacks), they should do so by calculating fees themselves;
since they're already charging well above market-rate,
over-estimating some won't have a large impact.

This commit adds a configuration knob to set the base fee
explicitly, defaulting to 1 sat, which appears to be market-rate
today.

[1] Note that due to an msat-vs-sat bug we currently actually
charge 1000x less than the calculated cost.

@TheBlueMatt TheBlueMatt added this to the 0.1 milestone Jun 29, 2021
@TheBlueMatt
Copy link
Collaborator Author

0.1 since none of our immediate users need payment forwarding, but without this we will spuriously fail payments.

@TheBlueMatt
Copy link
Collaborator Author

Oh whoops, this needs #970.

@TheBlueMatt TheBlueMatt modified the milestones: 0.1, 0.0.99 Jun 30, 2021
@TheBlueMatt
Copy link
Collaborator Author

Moving this back to 0.0.99 as I have another PR that should be based on it which needs to go in 0.0.99.

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Some attacks discussions though overall sounds good to me

// 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));
Copy link

Choose a reason for hiding this comment

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

Side-note, if we allow forwards outbound, i believe you still a timing attack to break the confidentiality of private channel by forwarding probing HTLC through a hub with tlv_payload's short_channel_id built from observed P2WSH outputs on the transaction logs. If the hub consumes a long delay to sent back an error, it means an update_add_htlc has been tried and rejected (for lack of payment_secret) by a counterparty on the other-side. If the hub consumes a short delay to sent back an error, it means the error has been generated by the hub, no such private channel is attached to the hub.

Does it work ? I've never tried it though i do remember talking about it with @naumenkogs a while back.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, for something with accept_forwards_to_priv_channels set we do have such a timing attack. At some point we need to move more things behind the forwarding delay (including claims, this, etc), its just low on the priority list :/.

Copy link

Choose a reason for hiding this comment

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

Tracked in #680, yeah agree low-priority for now...

/// update messages sent to notify all nodes of our updated relay fee.
///
/// Default value: 1000.
pub fee_base_msat: u32,
Copy link

Choose a reason for hiding this comment

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

Maybe add a note that default setting is matching market-rate as of June 2021 and that a routing node operator may lower it to increase competitivity as it's factor likely accounted by implementation autopilots at payment path building.

Also, we could add a different logic where the fee_base_msat increase in function of the channel load, thus making the last remaining HTLC slots more expensive to harden the liquidity cost of jamming the channel and divert the jamming flow to cheaper nodes ? Though I concede it's hard to evaluate utility of such mitigation without precise a jamming attacker, especially laying out attacker economic rationality of targeting this node.

Though i agree with you on not caring about fee_base_msat for flood-and-lood attacks, the cost risk of going onchain with any HTLC should be accounted as part of channel mitigation with some kind of compensation. A flood-and-lood attacker, motivated to steal HTLC, won't care about high-HTLC cost ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe add a note that default setting is matching market-rate as of June 2021

done.

Also, we could add a different logic where the fee_base_msat increase in function of the channel load, thus making the last remaining HTLC slots more expensive to harden the liquidity cost of jamming the channel and divert the jamming flow to cheaper nodes ? Though I concede it's hard to evaluate utility of such mitigation without precise a jamming attacker, especially laying out attacker economic rationality of targeting this node.

Yea, there's a few things I'd love to do with the feebase, but it doesn't belong in ChannelManager, IMO. I've been idly thinking of a separate crate that "manages" some stuff like this for routing nodes - auto-rebalances channels by intercepting payment events before handing them to the user, manages feerates based on various criteria, etc. Its a conversation for later, though.

Though i agree with you on not caring about fee_base_msat for flood-and-lood attacks, the cost risk of going onchain with any HTLC should be accounted as part of channel mitigation with some kind of compensation

Yea, but ultimately I think this is a question for the spec and pre-paying a fee followed by a fee rebate when its claimed, or something like that.

Copy link

Choose a reason for hiding this comment

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

I've been idly thinking of a separate crate that "manages" some stuff like this for routing nodes - auto-rebalances channels by intercepting payment events before handing them to the user, manages feerates based on various criteria, etc. Its a conversation for later, though.

Yeah we're aligned on this, better to get channel liquidity management in another crate!

Yea, but ultimately I think this is a question for the spec and pre-paying a fee followed by a fee rebate when its claimed, or something like that.

Yeap, same here beyond the scope of our implementation. Just to be sure we agree on what this new fee_base_msat is not covering.

@@ -224,6 +236,23 @@ pub struct UserConfig {
pub peer_channel_config_limits: ChannelHandshakeLimits,
/// Channel config which affects behavior during channel lifetime.
pub channel_options: ChannelConfig,
/// If a channel is not set to be publicly announced, we reject HTLCs which were set to be
Copy link

Choose a reason for hiding this comment

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

nit: maybe we should dissociate HTLC "rejection" at update_add_htlc processing from "forward failure", what do you think about "fail forward" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean in the docs? I feel like "reject" communicates whats going on, especially to a user who doesn't know about internal distinctions of HTLC rejections. "we fail HTLCs which were set" is slightly less clear to me, but also has roughly the same meaning in non-technical english.

Copy link

Choose a reason for hiding this comment

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

Well, i think it's yet another anticipation of introducing custom forward policy (PendingHTLCsRelayable), which would a new source of rejection cases. We'll cleanup at that time i guess.

/// ensure you are not exposed to any forwarding risk.
///
/// Note that because you cannot change a channel's announced state after creation, there is no
/// way to disable forwarding on public channels retroactively. Thus, in order to change a node
Copy link

Choose a reason for hiding this comment

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

I think #680 by allowing to load custom routing policy would make it easy to disallow forwarding in-flight on a named public channels by dropping the HTLC. Yep iptables for LN routing nodes!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, if users really want to do so, I guess we should let them, but they really shouldn't, ideally, want to do so - if you aren't "really" routing payments, your channel shouldn't be public. I wonder if we should only apply #680 to routing out over private channels? Anyway, we can debate it there.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from ab8b7aa to 0cc8feb Compare July 2, 2021 17:56
@TheBlueMatt
Copy link
Collaborator Author

Rebased on latest git now that #970 is merged.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from 690b17b to e8ac6a2 Compare July 3, 2021 00:38
@codecov
Copy link

codecov bot commented Jul 3, 2021

Codecov Report

Merging #975 (4cc0d9d) into main (12253e5) will increase coverage by 0.01%.
The diff coverage is 95.76%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #975      +/-   ##
==========================================
+ Coverage   90.74%   90.76%   +0.01%     
==========================================
  Files          60       60              
  Lines       30735    30914     +179     
==========================================
+ Hits        27891    28058     +167     
- Misses       2844     2856      +12     
Impacted Files Coverage Δ
lightning/src/util/config.rs 46.34% <0.00%> (-3.66%) ⬇️
lightning/src/util/ser_macros.rs 95.91% <ø> (ø)
lightning/src/ln/channel.rs 88.66% <76.00%> (-0.11%) ⬇️
lightning/src/ln/channelmanager.rs 84.85% <90.00%> (+0.02%) ⬆️
lightning-background-processor/src/lib.rs 95.00% <100.00%> (ø)
lightning/src/chain/channelmonitor.rs 90.80% <100.00%> (ø)
lightning/src/ln/functional_test_utils.rs 94.87% <100.00%> (+0.03%) ⬆️
lightning/src/ln/functional_tests.rs 97.33% <100.00%> (-0.05%) ⬇️
lightning/src/ln/onion_route_tests.rs 97.11% <100.00%> (+0.30%) ⬆️
lightning/src/util/test_utils.rs 82.54% <100.00%> (ø)
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 12253e5...4cc0d9d. Read the comment docs.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from e8ac6a2 to 2b4bb60 Compare July 3, 2021 01:13
@TheBlueMatt TheBlueMatt modified the milestones: 0.0.99, 0.0.100 Jul 3, 2021
@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from 2b4bb60 to be75d5f Compare July 5, 2021 00:39
@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from 78bc058 to b287400 Compare July 5, 2021 19:44
default_config.channel_options.cltv_expiry_delta = 6*6;
default_config.channel_options.announced_channel = true;
default_config.peer_channel_config_limits.force_announced_channel_preference = false;
default_config.own_channel_config.our_htlc_minimum_msat = 1000; // sanitization being done by the sender, to exerce receiver logic we need to lift of limit
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can't parse exerce -- could this comment be clarified?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I believe it was meant as exercise, but I have no idea what it means overall lol. Maybe @ariard remembers?

Copy link

Choose a reason for hiding this comment

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

IIRC, it's related to test_override_0msat_htlc_minimum, where we want to pass 0-msat value as a config and verify that our implementation-sanitization logic at channel opening bumps correctly to 1. See commit 9e03d2b. It's a bit gross, rational is for covering spec requirement in BOLT2

A receiving node:

    receiving an amount_msat equal to 0, OR less than its own htlc_minimum_msat:
        SHOULD fail the channel.

Yes exerce is to be understood as exercise, pardon my french :p

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! Updated the comment to describe it.

use ln::channelmanager::CLTV_FAR_FAR_AWAY;
use bitcoin::secp256k1;
fn test_fee_failures() {
// When we check for amount_below_minimum below, we want to test that we're using the *right*
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this check...? Can't tell if this comment is intentionally the same as test_onion_failure's

Also, could you add a link to the bug mentioned in the commit msg, to the commit msg?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Its copied from the below test, but, indeed, doesn't make sense here.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from cfc8efe to 3dcf5c4 Compare July 5, 2021 21:47
Comment on lines +276 to +281
// In an earlier version, we spuriously failed to forward payments if the expected feerate
// changed between the channel open and the payment.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a reference to the msat-vs-sat bug mentioned in the commit msg? Maybe missing something, but where can I find more information about the msat-vs-sat bug?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, literally current code is "we decide what we think the fee should be when we receive a payment to forward, and dont bother to update our announcements".....

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, sorry, an earlier version of this patchset had separated out the bugfixes for the two bugs, but now they're in the same, I've updated the commit message to be more clear.

channel_monitors.insert(monitor_a.get_funding_txo().0, &mut monitor_a);
channel_monitors.insert(monitor_b.get_funding_txo().0, &mut monitor_b);
<(BlockHash, ChannelManager<EnforcingSigner, &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestLogger>)>::read(&mut nodes_1_read, ChannelManagerReadArgs {
default_config: no_announce_cfg,
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be clear, users couldn't do exactly what this test is doing, to change their config on restart?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope, users can change the non-channel parts of the config at restart (but not at runtime...).

let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut no_announce_cfg = test_default_channel_config();
no_announce_cfg.channel_options.announced_channel = false;
no_announce_cfg.accept_forwards_to_priv_channels = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Guess we shouldn't need this because it's false by default?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, I just like making the test robust against a change to default values in the future.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from 69374ba to d303097 Compare July 5, 2021 22:49
///
/// Note that we do not check if you are currently connected to the given peer. If no
/// connection is available, the outbound `open_channel` message may fail to send, resulting in
/// the channel eventually being silently forgotten.
Copy link

Choose a reason for hiding this comment

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

What do you mean by "being silently forgotten", can't remember this case is covered by peer_disconnected ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is, it will be silently forgotten when we disconnect or restart.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from d303097 to 12aa66d Compare July 6, 2021 00:57
@TheBlueMatt
Copy link
Collaborator Author

Rebased and added a commit (that only makes sense if we merge this for 0.0.99) to use the same deserialization version check as here in the fields from #984.

@TheBlueMatt TheBlueMatt modified the milestones: 0.0.100, 0.0.99 Jul 6, 2021
@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 2 times, most recently from 923a69a to cb68789 Compare July 6, 2021 14:29
@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 3 times, most recently from 6041cd5 to 09d51ea Compare July 7, 2021 17:24
@TheBlueMatt
Copy link
Collaborator Author

Squashed with only the typo fix changed:

$ git diff-tree -U1 cb68789 09d51ea5
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index d738eefd..10d98deb 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -157,3 +157,3 @@ pub struct ChannelConfig {
 	/// The default value of a single satoshi roughly matches the market rate on many routing nodes
-	/// as of July 2021. Ajusting it upwards or downwards may change whether nodes route through
+	/// as of July 2021. Adjusting it upwards or downwards may change whether nodes route through
 	/// this node.
$

@naumenkogs
Copy link
Contributor

naumenkogs commented Jul 8, 2021

Concept ACK defaulting to 1 sat and making configurable :)

@TheBlueMatt TheBlueMatt mentioned this pull request Jul 8, 2021
Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

ACK 09d51ea, i did verify new functionality correctness and exercise their test coverage.

Few comments/remarks but no blockers for this PR, can be addressed in follow-ups.

@@ -4460,7 +4460,7 @@ fn is_unsupported_shutdown_script(their_features: &InitFeatures, script: &Script
return !script.is_p2pkh() && !script.is_p2sh() && !script.is_v0_p2wpkh() && !script.is_v0_p2wsh()
}

const SERIALIZATION_VERSION: u8 = 1;
const SERIALIZATION_VERSION: u8 = 2;
Copy link

Choose a reason for hiding this comment

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

One follow-up is documenting our serialization philosophy in a doc/serialization.md, explain when/why we introduce changes, the odd/even scheme with TLV, that bindings should stay in-sync and what actions user should take in consequence?

Maybe it could be part of LDK docs, but when it's a library interface I would favor the stability guarantees to be documented in-tree ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea, we're kinda still figuring it out and doing a case-by-case "what do you want the old clients to do". Def could add to CONTRIBUTING that we have to meticulously document any backwards compat concerns in the CHANGELOG :)

/// this node.
///
/// Default value: 1000.
pub fee_base_msat: u32,
Copy link

Choose a reason for hiding this comment

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

One readability improvement could be to make it matching the method outbound_forwarding_fee_base_msat. Even precise the type of packet forwarded, HTLC (we can't exclude to have different default routing policies if some future packets are more CPU-intensive like PTLC/n-ary outcome DLC due to MuSig rounds). And maybe align get_fee_proportional_millionths() naming, like get_outbound_forwarding_fee_proportional_msat?

Yep comment quite inspired by Core inconsistency around its units either used internally or exposed to user (kilo-sat per vbytes, btc per vbyte, WU, packages evaluated in KvB, ...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just mentioned its the outbound leg in the docs, adding it to the variable name seemed like a useless distinction.

@@ -197,6 +208,7 @@ impl Default for ChannelConfig {
fn default() -> Self {
ChannelConfig {
fee_proportional_millionths: 0,
fee_base_msat: 1000,
Copy link

Choose a reason for hiding this comment

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

Note, mutating to 1001 breaks test_priv_forwarding_rejection but the lower-boundary value 999 doesn't break test coverage :/

edit: but covered by fuzzer :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, well that's mostly cause test_priv_forwarding_rejection isn't intended to test the default fee value :). The onion_route_tests and others will fail if we don't properly check the forwarding fee, though.


assert!(nodes[0].node.list_usable_channels()[0].is_public);
assert_eq!(nodes[1].node.list_usable_channels().len(), 2);
assert!(!nodes[2].node.list_usable_channels()[0].is_public);
Copy link

Choose a reason for hiding this comment

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

Hmmmm, i was suggesting to add the following diff but I do have non-deterministic failure, i think it's inherited from list_usable_channels()

diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 29071ab0..11154f5c 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -7960,6 +7960,8 @@ fn test_priv_forwarding_rejection() {
 
        assert!(nodes[0].node.list_usable_channels()[0].is_public);
        assert_eq!(nodes[1].node.list_usable_channels().len(), 2);
+       assert!(nodes[1].node.list_usable_channels()[0].is_public);
+       assert!(!nodes[1].node.list_usable_channels()[1].is_public);
        assert!(!nodes[2].node.list_usable_channels()[0].is_public);
 
        // We should always be able to forward through nodes[1] as long as its out through a public

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The ordering of the channels is non-deterministic - its based on the random seed used for the hashmap, even, so this will fail randomly.

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from 09d51ea to f557df9 Compare July 9, 2021 00:19
@TheBlueMatt
Copy link
Collaborator Author

TheBlueMatt commented Jul 9, 2021

Squashed a few trivial changes to address all review feedback, will take after CI. Full diff from Val's review:

$ $ git diff-tree -U1  cb68789 7658421c
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 38e12b84..93670f09 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -340,3 +340,3 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
 			let mut config = UserConfig::default();
-			config.channel_options.fee_proportional_millionths = 0;
+			config.channel_options.forwarding_fee_proportional_millionths = 0;
 			config.channel_options.announced_channel = true;
@@ -359,3 +359,3 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
 			let mut config = UserConfig::default();
-			config.channel_options.fee_proportional_millionths = 0;
+			config.channel_options.forwarding_fee_proportional_millionths = 0;
 			config.channel_options.announced_channel = true;
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index b03bde0d..9398dcb0 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -361,3 +361,3 @@ pub fn do_test(data: &[u8], logger: &Arc<dyn Logger>) {
 	let mut config = UserConfig::default();
-	config.channel_options.fee_proportional_millionths =  slice_to_be32(get_slice!(4));
+	config.channel_options.forwarding_fee_proportional_millionths =  slice_to_be32(get_slice!(4));
 	config.channel_options.announced_channel = get_slice!(1)[0] != 0;
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 93197d49..87f4e6ea 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -3427,3 +3427,3 @@ impl<Signer: Sign> Channel<Signer> {
 	pub fn get_fee_proportional_millionths(&self) -> u32 {
-		self.config.fee_proportional_millionths
+		self.config.forwarding_fee_proportional_millionths
 	}
@@ -3501,3 +3501,3 @@ impl<Signer: Sign> Channel<Signer> {
 	pub fn get_outbound_forwarding_fee_base_msat(&self) -> u32 {
-		self.config.fee_base_msat
+		self.config.forwarding_fee_base_msat
 	}
@@ -4490,3 +4490,3 @@ impl<Signer: Sign> Writeable for Channel<Signer> {
 		// deserializers, but we will read the version in the TLV at the end instead.
-		self.config.fee_proportional_millionths.write(writer)?;
+		self.config.forwarding_fee_proportional_millionths.write(writer)?;
 		self.config.cltv_expiry_delta.write(writer)?;
@@ -4652,3 +4652,6 @@ impl<Signer: Sign> Writeable for Channel<Signer> {
 		self.counterparty_max_htlc_value_in_flight_msat.write(writer)?;
+
+		// Note that this field is ignored by 0.0.99+ as the TLV Optional variant is used instead.
 		self.counterparty_selected_channel_reserve_satoshis.unwrap_or(0).write(writer)?;
+
 		self.counterparty_htlc_minimum_msat.write(writer)?;
@@ -4656,2 +4659,4 @@ impl<Signer: Sign> Writeable for Channel<Signer> {
 		self.counterparty_max_accepted_htlcs.write(writer)?;
+
+		// Note that this field is ignored by 0.0.99+ as the TLV Optional variant is used instead.
 		self.minimum_depth.unwrap_or(0).write(writer)?;
@@ -4709,3 +4714,3 @@ impl<'a, Signer: Sign, K: Deref> ReadableArgs<&'a K> for Channel<Signer>
 			// Read the old serialization of the ChannelConfig from version 0.0.98.
-			config.as_mut().unwrap().fee_proportional_millionths = Readable::read(reader)?;
+			config.as_mut().unwrap().forwarding_fee_proportional_millionths = Readable::read(reader)?;
 			config.as_mut().unwrap().cltv_expiry_delta = Readable::read(reader)?;
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 29071ab0..986ff9d8 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -1900,3 +1900,3 @@ fn test_channel_reserve_holding_cell_htlcs() {
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 239;
+	config.channel_options.forwarding_fee_base_msat = 239;
 	let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config.clone())]);
@@ -5353,3 +5353,3 @@ fn test_duplicate_payment_hash_one_failure_one_success() {
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 196;
+	config.channel_options.forwarding_fee_base_msat = 196;
 	let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs,
@@ -5545,3 +5545,3 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 196;
+	config.channel_options.forwarding_fee_base_msat = 196;
 	let node_chanmgrs = create_node_chanmgrs(6, &node_cfgs,
@@ -6400,3 +6400,3 @@ fn test_fail_holding_cell_htlc_upon_free_multihop() {
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 196;
+	config.channel_options.forwarding_fee_base_msat = 196;
 	let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config.clone())]);
@@ -7968,2 +7968,4 @@ fn test_priv_forwarding_rejection() {
 
+	// ... however, if we send to nodes[2], we will have to pass the private channel from nodes[1]
+	// to nodes[2], which should be rejected:
 	let (our_payment_preimage, our_payment_hash, our_payment_secret) = get_payment_preimage_hash!(nodes[2]);
diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs
index e325ffce..62086430 100644
--- a/lightning/src/ln/onion_route_tests.rs
+++ b/lightning/src/ln/onion_route_tests.rs
@@ -257,3 +257,3 @@ fn test_fee_failures() {
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 196;
+	config.channel_options.forwarding_fee_base_msat = 196;
 
@@ -308,3 +308,3 @@ fn test_onion_failure() {
 	let mut config = test_default_channel_config();
-	config.channel_options.fee_base_msat = 196;
+	config.channel_options.forwarding_fee_base_msat = 196;
 
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index d738eefd..2e9e8e03 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -145,3 +145,4 @@ impl Default for ChannelHandshakeLimits {
 pub struct ChannelConfig {
-	/// Amount (in millionths of a satoshi) the channel will charge per transferred satoshi.
+	/// Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound
+	/// over the channel.
 	/// This may be allowed to change at runtime in a later update, however doing so must result in
@@ -150,5 +151,5 @@ pub struct ChannelConfig {
 	/// Default value: 0.
-	pub fee_proportional_millionths: u32,
-	/// Amount (in milli-satoshi) the channel will charge for each transferred HTLC, in excess of
-	/// fee_proportional_millionths.
+	pub forwarding_fee_proportional_millionths: u32,
+	/// Amount (in milli-satoshi) charged for payments forwarded outbound over the channel, in
+	/// excess of [`forwarding_fee_proportional_millionths`].
 	/// This may be allowed to change at runtime in a later update, however doing so must result in
@@ -157,3 +158,3 @@ pub struct ChannelConfig {
 	/// The default value of a single satoshi roughly matches the market rate on many routing nodes
-	/// as of July 2021. Ajusting it upwards or downwards may change whether nodes route through
+	/// as of July 2021. Adjusting it upwards or downwards may change whether nodes route through
 	/// this node.
@@ -161,3 +162,5 @@ pub struct ChannelConfig {
 	/// Default value: 1000.
-	pub fee_base_msat: u32,
+	///
+	/// [`forwarding_fee_proportional_millionths`]: ChannelConfig::forwarding_fee_proportional_millionths
+	pub forwarding_fee_base_msat: u32,
 	/// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded over
@@ -212,4 +215,4 @@ impl Default for ChannelConfig {
 		ChannelConfig {
-			fee_proportional_millionths: 0,
-			fee_base_msat: 1000,
+			forwarding_fee_proportional_millionths: 0,
+			forwarding_fee_base_msat: 1000,
 			cltv_expiry_delta: 6 * 12, // 6 blocks/hour * 12 hours
@@ -222,3 +225,3 @@ impl Default for ChannelConfig {
 impl_writeable_tlv_based!(ChannelConfig, {
-	(0, fee_proportional_millionths, required),
+	(0, forwarding_fee_proportional_millionths, required),
 	(2, cltv_expiry_delta, required),
@@ -226,3 +229,3 @@ impl_writeable_tlv_based!(ChannelConfig, {
 	(6, commit_upfront_shutdown_pubkey, required),
-	(8, fee_base_msat, required),
+	(8, forwarding_fee_base_msat, required),
 });
$

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch 3 times, most recently from c3be826 to 7658421 Compare July 9, 2021 00:33
This was missed prior to 0.0.98, so requires a
backwards-compatibility wrapper inside the `Channel` serialization
logic, but it's not very complicated to do so.
Currently the base fee we apply is always the expected cost to
claim an HTLC on-chain in case of closure. This results in
significantly higher than market rate fees [1], and doesn't really
match the actual forwarding trust model anyway - as long as
channel counterparties are honest, our HTLCs shouldn't end up
on-chain no matter what the HTLC sender/recipient do.

While some users may wish to use a feerate that implies they will
not lose funds even if they go to chain (assuming no flood-and-loot
style attacks), they should do so by calculating fees themselves;
since they're already charging well above market-rate,
over-estimating some won't have a large impact.

Worse, we current re-calculate fees at forward-time, not based on
the fee we set in the channel_update. This means that the fees
others expect to pay us (and which they calculate their route based
on), is not what we actually want to charge, and that any attempt
to forward through us is inherently race-y.

This commit adds a configuration knob to set the base fee
explicitly, defaulting to 1 sat, which appears to be market-rate
today.

[1] Note that due to an msat-vs-sat bug we currently actually
    charge 1000x *less* than the calculated cost.
@TheBlueMatt
Copy link
Collaborator Author

TheBlueMatt commented Jul 9, 2021

Turns out upstream changes made the new tests fail, applied this diff on top of a clean rebase.

diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 8e5e5839..feb8bd7e 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -7955,9 +7955,11 @@ fn test_priv_forwarding_rejection() {
        connect_blocks(&nodes[1], CHAN_CONFIRM_DEPTH - 1);
        confirm_transaction_at(&nodes[2], &tx, conf_height);
        connect_blocks(&nodes[2], CHAN_CONFIRM_DEPTH - 1);
+       let as_funding_locked = get_event_msg!(nodes[1], MessageSendEvent::SendFundingLocked, nodes[2].node.get_our_node_id());
        nodes[1].node.handle_funding_locked(&nodes[2].node.get_our_node_id(), &get_event_msg!(nodes[2], MessageSendEvent::SendFundingLocked, nodes[1].node.get_our_node_id()));
-       let funding_locked = get_event_msg!(nodes[1], MessageSendEvent::SendFundingLocked, nodes[2].node.get_our_node_id());
-       nodes[2].node.handle_funding_locked(&nodes[1].node.get_our_node_id(), &funding_locked);
+       get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, nodes[2].node.get_our_node_id());
+       nodes[2].node.handle_funding_locked(&nodes[1].node.get_our_node_id(), &as_funding_locked);
+       get_event_msg!(nodes[2], MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id());
 
        assert!(nodes[0].node.list_usable_channels()[0].is_public);
        assert_eq!(nodes[1].node.list_usable_channels().len(), 2);
@@ -8054,8 +8056,10 @@ fn test_priv_forwarding_rejection() {
 
        nodes[0].node.peer_connected(&nodes[1].node.get_our_node_id(), &msgs::Init { features: InitFeatures::known() });
        nodes[1].node.peer_connected(&nodes[0].node.get_our_node_id(), &msgs::Init { features: InitFeatures::empty() });
-       nodes[1].node.handle_channel_reestablish(&nodes[0].node.get_our_node_id(), &get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, nodes[1].node.get_our_node_id()));
-       nodes[0].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(), &get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, nodes[0].node.get_our_node_id()));
+       nodes[1].node.handle_channel_reestablish(&nodes[0].node.get_our_node_id(),
+               &get_two_event_msgs!(nodes[0], MessageSendEvent::SendChannelReestablish, MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id()).0);
+       nodes[0].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(),
+               &get_two_event_msgs!(nodes[1], MessageSendEvent::SendChannelReestablish, MessageSendEvent::SendChannelUpdate, nodes[0].node.get_our_node_id()).0);
 
        nodes[1].node.peer_connected(&nodes[2].node.get_our_node_id(), &msgs::Init { features: InitFeatures::known() });
        nodes[2].node.peer_connected(&nodes[1].node.get_our_node_id(), &msgs::Init { features: InitFeatures::empty() });
-       nodes[1].node.handle_channel_reestablish(&nodes[0].node.get_our_node_id(),
-               &get_two_event_msgs!(nodes[0], MessageSendEvent::SendChannelReestablish, MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id()).0);
-       nodes[0].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(),
-               &get_two_event_msgs!(nodes[1], MessageSendEvent::SendChannelReestablish, MessageSendEvent::SendChannelUpdate, nodes[0].node.get_our_node_id()).0);
+       let as_reestablish = get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, nodes[1].node.get_our_node_id());
+       let bs_reestablish = get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, nodes[0].node.get_our_node_id());
+       nodes[1].node.handle_channel_reestablish(&nodes[0].node.get_our_node_id(), &as_reestablish);
+       nodes[0].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(), &bs_reestablish);
+       get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id());
+       get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, nodes[0].node.get_our_node_id());
 
        nodes[1].node.peer_connected(&nodes[2].node.get_our_node_id(), &msgs::Init { features: InitFeatures::known() });
        nodes[2].node.peer_connected(&nodes[1].node.get_our_node_id(), &msgs::Init { features: InitFeatures::empty() });
-       nodes[2].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(), &get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, nodes[2].node.get_our_node_id()));
-       nodes[1].node.handle_channel_reestablish(&nodes[2].node.get_our_node_id(), &get_event_msg!(nodes[2], MessageSendEvent::SendChannelReestablish, nodes[1].node.get_our_node_id()));
+       let bs_reestablish = get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, nodes[2].node.get_our_node_id());
+       let cs_reestablish = get_event_msg!(nodes[2], MessageSendEvent::SendChannelReestablish, nodes[1].node.get_our_node_id());
+       nodes[2].node.handle_channel_reestablish(&nodes[1].node.get_our_node_id(), &bs_reestablish);
+       nodes[1].node.handle_channel_reestablish(&nodes[2].node.get_our_node_id(), &cs_reestablish);
+       get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, nodes[2].node.get_our_node_id());
+       get_event_msg!(nodes[2], MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id());
 
        nodes[0].node.send_payment(&route, our_payment_hash, &Some(our_payment_secret)).unwrap();
        check_added_monitors!(nodes[0], 1);

@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from 7658421 to 352594b Compare July 9, 2021 01:04
Private nodes should never wish to forward HTLCs at all, which we
support here by disabling forwards out over private channels by
default. As private nodes should not have any public channels, this
suffices, without allowing users to disable forwarding over
channels announced in the routing graph already.

Closes lightningdevkit#969
Instead of interpreting the backwards compatibility data in Channel
serialization, use the serialization version bump present in 0.0.99
as the flag to indicate if a channel should be read in backwards
compatibility.
@TheBlueMatt TheBlueMatt force-pushed the 2021-06-fix-fee-calc branch from 352594b to 4cc0d9d Compare July 9, 2021 01:33
@TheBlueMatt TheBlueMatt merged commit e04a1b3 into lightningdevkit:main Jul 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants