Good morning Lightning world,
This was originally written for C-Lightning, hence the C-Lightning-isms, but I
realized the general principles were valid for all LN implementations.
---
First, let us consider some C-Lightning node who, for some reason, only has
unpublished channels to the network (note: in my opinion, ***unpublished
channels delenda est!***). Let us suppose this node wishes to receive some
amount of funds, from multiple different payers.
Now, the payee node knows MPP exists, and thus, it is not very concerned that
it has insufficient incoming capacity in a ***single*** channel to accept one
of the payments. Reasoning that MPP exists, it considers instead the *sum
total* of all incoming capacities. It sees that it has sufficient *sum total*
incoming capacity that it can accept all payments from all payers, with some to
spare.
However, at the channel-level detail, *none* of the channels has sufficient
capacity to accept a single one of the payments the C-Lightning node now
expects to receive, in full. So, what should the C-Lightning node put as
routehints in a generated invoice?
Another, is that we should note that I described "payers" with an "s" at the
end. In the English language as commonly used by puny humans, adding an "s" at
the end of a noun is a common (but not universal) convention implying that the
subject under discussion is actually composed of more than one item. So not
only must the C-Lightning node (all of whose channels are unpublished, thus
cannot be paid unless it reveals at least some of the channels as routehints in
the invoice) make an invoice with sufficient routehints to get an MPP
delivered, it must make *multiple* invoices, such that the invoices it issues
have as little overlap in their routehints as possible
Otherwise, if we provide the **same** set of routehints in invoices to two
**different** payers, both of them will interfere with each other, possibly to
the point that neither can pay to the payee (since e.g. some of the available
capacity is taken up by parts of one payment while the rest is taken up by
parts of the other payment, and neither payment can then complete in full due
to this deadlock).
This is important in case the C-Lightning node receiver is completely
unannounced (i.e. has only unpublished channels). The payers cannot fall back
to just trying directly routing on a different incoming channel of the
receiver, as they may not have any knowledge of such channels.
I suggest, then, that the C-Lightning node that wants to get paid, and uses
only unpublished channels, should use a Round-Robin strategy for selecting
routehints.
To implement this, the `invoice` command maintains a persistent list of all
channels the node has. When a newly funded channel is locked-in by both sides
(transitions to `CHANNELD_NORMAL`) it is simply appended to this list. When a
channel leaves `CHANNELD_NORMAL`it is simply removed from this list. Let us
call this the round-robin list.
Then, when the `invoice` command is invoked, it generates routehints by
executing a loop:
* Maintain variables: a removed list (initially empty), a running sum amount
(initially `AMOUNT_MSAT(0)`), and a set of proposed routehints (initially empty)
* Round-robin: Remove a channel from the front of the round-robin list and put
it in the end of the removed list.
* If the channel has no incoming capacity, skip to the top of the loop again
(`continue;`).
* If the channel is a dead end (i.e. connects to a node which has no published
channels elsewhere), skip as well (`continue;`).
* Copy the scid and any other details to the set of proposed routehints.
* Add the incoming capacity of the taken channel to the running sum. If the
running sum now exceeds the amount to be paid, append the removed list to the
current round-robin list and leave the loop.
* If the round-robin list is now empty, exit the loop (append the removed list
to the current empty round-robin list) and report a `warning_capacity` that it
looks like there is insufficient incoming capacity for this invoice.
On exit of the above loop, we sort the set of proposed routehints, from highest
incoming capacity to lowest, and use that set as the routehints for the invoice.
(It would probably be a good idea as well to persist the round-robin list)
The above ensures that as much as possible, different invoices will have
different sets of channels routehinted (due to the round-robin behavior), but
gracefully degrades (some overlap in routehints) if the number of channels
available is low.
An issue is privacy. A probing fake payer could spam the receiver with
multiple requests for invoices it never intends to pay. This can let the fake
payer discover, at minimum, the entry points used by the payee to connect to
the public network (which cannot be removed, even if you somehow obscure the
actual short-channel-IDs of the payee, since the payer has to know *what* node
to reach to get to the payee), which the payer can then probe or otherwise hack
or buy out in order to extract information about the economic activity of the
payee (due to the Axiom of Terminus, any forward that goes *to* an unpublished
channel, *is* the final hop to the actual final destination, so the entry
points have accurate data on incoming payments of the unpublished payee via the
channel with them). If the payer simulates multiple payers (i.e. Sybils) the
payee cannot attempt to reuse channels in routehints to the different simulated
payers, as that would degrade the ability to pay since th
e capaci
ty of the channels has to be shared by the different payers if those payers
were not sockpuppets. Thus the lie of improved privacy by unpublished channels
is revealed, and therefore, in my opinion, unpublished channels delenda est.
----
Now, let us turn our attention to the payer of such an invoice.
Currently, our `paymod` system uses a single sequence of routehints for all
sub-payments of an MPP.
Thus, if a payment is split in two, both sub-payments will start executing on
the same routehint.
Yet as considered above, the point of multiple routehints is that not a single
one has sufficient capacity to receive, and the multiple routehints are
provided which in total are needed to receive all the incoming funds.
Thus, I suggest as well, that sub-payments should have different sequences of
routehints to try.
For example, currently, for sub-payments 0 and 1, they will try routehints A,
B, C, D in this order:
0 1
A A
B B
C C
D D
Instead of using the same shared sequence of routehints, on splitting, the
split payments should use:
0 1
A B
C D
B A
D C
That is, sub-payment 0 should try routehints in the order A, C, B, D, while the
sub-payment 1 should try routehints in the order B, D, A, C.
The reasoning is the same as with the argument for round-robin routehint
selection: by having different sub-payments initially try different routehints,
we improve our success by reducing their overlap in the use of shared
resources, and therefore reduce the risk of deadlock (especially if the
sub-payments are adaptively split into even more sub-payments).
We can do this by a sort of round-robin, as well. For each sub-payment we
initially create an empty list of routehints. Then we "spread" the routehints
to each sub-payment.
* If there are more sub-payments than routehints, then we repeat the routehints
until we have given one routehint to each sub-payment.
* If there are more routehints than sub-payments, then we repeat the
sub-payments until we have completed the routehints.
After the above initial distribution, we then go through all sub-payments, and
for each sub-payment, append routehints in the original order, but skipping
those that were already given to that sub-payment in the above initial
distribution.
For example, suppose we are distributing 3 routehints A B C to two sub-payments
0 1. We do the initial distribution:
0 1
A B
C
And then we give each sub-payment the rest of the routehints:
0 1
A B
C A
B C
Another example, where we are distributing 2 routehints A B to four
sub-payments 0 1 2 3. Our initial distribution is:
0 1 2 3
A B A B
Then we give each sub-payment the rest of the routehints:
0 1 2 3
A B A B
B A B A
Thus, each sub-payment gets the entire list of routehints, but in a different
order.
----
Finally: the same argument --- that sub-payments should avoid using the same
set of resources as much as possible -- applies to the *first* hop just as much
as the *last* hop.
Thus, one possibility is to also maintain a list of outgoing channels for each
sub-payment, which sub-payments try in round-robin order. And again, each
sub-payment should get a different order from each other in a coordinated
fashion which reduces their interference.
We do this by, at the *start* of the payment before we split it up, taking all
local channels and shuffling them. Then we distribute the outgoing channels
with the same algorithm as the above.
Then, when a sub-payment executes, we use a similar algorithm as the `invoice`
case where it selects routehints.
* Maintain a removed list (initially empty).
* Round-robin: Remove the channel at the front of the round-robin list, append
to removed list.
* If the round-robin list is empty, then we should append the removed list to
the round-robin list and consider splitting the payment.
* If it has insufficient outgoing capacity, skip.
* If it is a dead end, skip.
* Otherwise it has sufficient capacity, break out of the loop (append the
removed list to the round-robin list) and try routing via that channel.
--
Regards,
ZmnSCPxj
_______________________________________________
Lightning-dev mailing list
[email protected]
https://lists.linuxfoundation.org/mailman/listinfo/lightning-dev