Skip to content
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

Update routing hints upon insufficient fees failure. #2151

Conversation

Bluetegu
Copy link
Contributor

@Bluetegu Bluetegu commented Nov 5, 2018

This pull request address issue #2066

Reconstruct the problem:
Setup composed of 3 nodes, A -> B -> C, where B -> C is private.
Create an invoice on C (C:addinvoice with private flag)
Increase fee on B->C (B:updatechanpolicy to a higher fee)
Attempt to pay invoice from A (A:sendpayment --pay_req=xxx)
Receive payment error ("unable to route payment to destination: FeeInsufficient ... ")

After the fix the payment goes through.

A unit test on the 'basic graph' was added. The test adds a private channel between son goku and elst, with lower fees compared with the alternative routes from roasbeef to elst. A fee error is returned on first attempt to route through the private channel, with a higher fee, but still better compared with the alternative routes. The test checks that the after the update the payment is still routed through the private channel but with the updated higher fees.

A bug in the existing test TestSendPaymentErrorRepeatedFeeInsufficient was fixed too. I did a minimal fix. I believe this test can be further improved. The major issue was that the simulated error returned was rejected due to signature failure, and hence didn't simulate correctly the insufficient fees error as intended. In order to correctly sign the errors, an optional private key attribute was added to the basic graph and its parser.

@halseth halseth added routing payments Related to invoices/payments P3 might get fixed, nice to have labels Nov 7, 2018
@halseth halseth added this to the 0.6 milestone Nov 7, 2018
@@ -291,6 +291,27 @@ func generateBandwidthHints(sourceNode *channeldb.LightningNode,
return bandwidthHints, nil
}

// UpdateEdgePolicy updates the edge policy of the routing hints kept in the
// payment session. The number of hints updated is returned.
func (p *paymentSession) UpdateEdgePolicy(e *channeldb.ChannelEdgePolicy) uint32 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems the return value is used nowhere and can just as well be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -1838,6 +1840,8 @@ func (r *ChannelRouter) sendPayment(payment *LightningPayment,
// newly updated fees.
case *lnwire.FailFeeInsufficient:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not the only case where we can get back a channel update. For example, FailIncorrectCltvExpiry. I think we should try to update additionalEdges also in those cases.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can the update maybe be done inside applyChannelUpdate ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

routing/router_test.go Show resolved Hide resolved
routing/pathfind_test.go Show resolved Hide resolved
routing/router_test.go Show resolved Hide resolved
t.Parallel()

const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are currently two ways of writing these kind of path finding tests. One is the way you did it, using the json file. The other option is used in for example TestChannelUpdateValidation. It allows programmatically building the test graph and generates some values like the keys by itself (as the actual value isn't important for the tests, it just needs a value). You could consider using that instead. I think it is clearer than keep adding new channels to the json file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that you will still have the problem of needing a private key even if you use TestChannelUpdateValidation approach, as the validation of the error message is part of the tested code. Anyhow, I like the approach of using the basic graph in many tests all based on a non trivial topology. It requires the writer of the test to rethink whether the logic indeed covers all scenarios, beyond the simplest topology needed for testing the particular feature or fix she was aiming to do.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's okay, just wanted to mention the option.

@joostjager
Copy link
Collaborator

Needs to be rebased

@joostjager
Copy link
Collaborator

Also, have a look at the commit message format described in https://dev.lightning.community/contribute/. We add the subsystem prefix to the message.

@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch from 57fbf15 to cc5444f Compare December 5, 2018 16:56
@Bluetegu
Copy link
Contributor Author

Bluetegu commented Dec 5, 2018

Thanks for the review!
I rebased and changed the commit message(s). The code in router.go looks much better all around.
I addressed most of the comments. Please see responses to individual comments inline.

@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch 5 times, most recently from 1ba5022 to 4b1f171 Compare December 8, 2018 20:51
@Bluetegu
Copy link
Contributor Author

Bluetegu commented Dec 8, 2018

I rebased again, but for some reason the third CI test (coverage) fails in auto pilot and I don't think this has anything to do with the code modification in this PR. I forced push twice already and got the same error there.

@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch from 4b1f171 to 2ef99a0 Compare December 9, 2018 16:58
@@ -1818,6 +1820,12 @@ func (r *ChannelRouter) sendPayment(payment *LightningPayment,
update *lnwire.ChannelUpdate,
pubKey *btcec.PublicKey) {

err := ValidateChannelUpdateAnn(pubKey, update)
if err != nil {
log.Errorf("Unable to validate channel update: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This line and others too exceed the max line width of 80 chars (project coding standard).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The log line is of 77 width. I believe I kept the <80 width everywhere.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I have it at 96 chars. Do you have tab size 8?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tab size 4. Using vscode.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The standard is 80 chars max @ tab size 8.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about that. Fixed.

@@ -1818,6 +1820,12 @@ func (r *ChannelRouter) sendPayment(payment *LightningPayment,
update *lnwire.ChannelUpdate,
pubKey *btcec.PublicKey) {

err := ValidateChannelUpdateAnn(pubKey, update)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this added here? Validation is also done inside r.applyChannelUpdate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree this is ugly. My excuse it that I'm trying to minimize the changes I do in code to those mandatory for this PR. Therefore, if validation fails, processChannelUpdateAndRetry should immediately return, and should not update paySession at all (neither update hints or report edge policy failure), so the check must be there. However we can not rely on the error returned by applyChannelUpdate, as it may well error in other scenarios (i.e. not validation errors) which are perfectly ok, and do require retries. Initially I thought of doing the validation much earlier, i.e. before the switch on the error types, but this looks like a change that is too major for this PR; hence my feeling was that adding validation here is the right way to go, with only bad consequence being that its checked twice.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is ok to not update the payment session if the graph update fails. ReportEdgeFailure is important to call in case of a validation error. If we get an incorrect update we don't want to try that channel anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. I removed the extra validation and returned if applyChannelUpdate fails after the edge is pruned.

I'm uncomfortable with the handling of error messages that fail in signature validation, as they do prune edges. Doesn't that allow an attacker to send error messages and cause the routing to fail?

FeeBaseMSat: uBaseFee,
FeeProportionalMillionths: uFeeRate,
})

Copy link
Collaborator

Choose a reason for hiding this comment

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

You've added it in here, but for other cases where r.applyChannelUpdate is called directly, the payment session isn't updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My idea is that the hints are updated only when a retry is aimed at. It now handles all those cases (not only insufficient fees). Otherwise the vertex or channel is removed completely, and there is no need for the update. I thought that is the whole purpose of defining processChannelUpdateAndRetry (which includes the paySession in its closure).
Said in another way, it doesn't seem right to me to plug it into applyChannelUpdate, exactly because this function is called for changing database edges/vertexes (and don't have paySession in closure).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think you're right. All other calls to applyChannelUpdate prune immediately, so those edges will never show up in subsequent payment attempts anymore.

routing/router_test.go Show resolved Hide resolved
routing/router_test.go Show resolved Hide resolved
t.Parallel()

const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's okay, just wanted to mention the option.

routing/router_test.go Show resolved Hide resolved
// run over hints and update policy if edge id matches
for _, edges := range p.additionalEdges {
for _, edge := range edges {
if edge.ChannelID == e.ChannelID {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you updating the channel in both directions? I think you may also need to match the pubkey in additional edges to update only a single direction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The channel is updated only in one direction. That is, the node generates an error on the outgoing channel policy, and you update the hints accordingly. Am I missing something here?

Copy link
Collaborator

@joostjager joostjager Dec 10, 2018

Choose a reason for hiding this comment

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

I mean, suppose you have two additional edges relating to the same channel id but in opposite direction. They will both be updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I changed it such that it takes direction into account too, such that policy will be updated only if channel ID and direction both match.
I think this also fixes a general hidden bug in routing through hint hops, not related to this PR, as additionalEdges policies didn't take into account direction. That is, the Flags were not initialized in NewPaymentSession, and routing assumes the direction is initialized in all edge policies.

@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch 7 times, most recently from cf3f300 to a0ccce4 Compare December 10, 2018 19:41
@Bluetegu
Copy link
Contributor Author

Ready for inspection :-)
By some miracle the cover test passed too.
Please see inline my response to the review issues.
Thanks!

routing/router_test.go Show resolved Hide resolved
if edge.ChannelID == e.ChannelID &&
edge.Flags&lnwire.ChanUpdateDirection ==
e.Flags&lnwire.ChanUpdateDirection {
edge.FeeBaseMSat = e.FeeBaseMSat
Copy link
Collaborator

Choose a reason for hiding this comment

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

New line after wrapped line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

var direction lnwire.ChanUpdateFlag
if failedEdge.direction == 1 {
direction = lnwire.ChanUpdateDirection
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

failedEdge.direction can be used directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure. failedEdge direction is unit8 while Flags are unit16.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, you may need to do a type cast, but the if statement is not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// UpdateEdgePolicy updates the edge policy of the routing hints kept in the
// payment session.
func (p *paymentSession) UpdateEdgePolicy(e *channeldb.ChannelEdgePolicy) {

Copy link
Collaborator

Choose a reason for hiding this comment

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

remove newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// payment session.
func (p *paymentSession) UpdateEdgePolicy(e *channeldb.ChannelEdgePolicy) {

// run over hints and update policy if edge id and direction matches
Copy link
Collaborator

Choose a reason for hiding this comment

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

Capital at beginning of sentence. Period at end.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

log.Error(err)
return err
}
if exists {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a fix in itself? Ideally this could be yet another separate commit, but if you find it too small for that, it's fine with me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to add it as otherwise you would get errors on applyChannelUpdate when trying to update private channels hops.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think maybe the proper fix here is something like this: ad92e5c (feel free to cherry-pick it into this PR if needed).

I believe then you wouldn't have to flip the AssumeChannelValid in the config during tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Created #2549

Copy link
Contributor

Choose a reason for hiding this comment

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

This can be removed now that #2549 is merged.

uBaseFee := lnwire.MilliSatoshi(update.BaseFee)
uFeeRate := lnwire.MilliSatoshi(update.FeeRate)
uMinHTLC := lnwire.MilliSatoshi(
update.HtlcMinimumMsat)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This vars are introduced because of line length I assume. But maybe that indicates that now is the time to extract this closure to a separate function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. But I don't get much if I move it to a separate function defined within the closure, as the names are so long here that it doesn't make it much better. I rather leave it as is. Defining a separate function altogether for an internal code that is called once is not something I personally prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There are plenty of internal functions that are called only once, it does help to make the code more understandable. Inline functions can be useful especially if a lot of local vars are bound.

How about just creating a function taking failedEdge and update and returning the ChannelEdgePolicy? Or better, updating the signature of UpdateEdgePolicy to take failedEdge and update. Also consistent with other pay session funcs that take an edgeLocator too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is the way to go. Thanks!


// The route should pass via the private channel
if !route.containsChannel(privateChannelID) {
t.Fatalf("route did not pass through private channel between pham nuwen " +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Line length.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

routing/router_test.go Show resolved Hide resolved
@@ -219,9 +226,9 @@ func (m *missionControl) NewPaymentSession(routeHints [][]HopHint,
hopHint.FeeProportionalMillionths,
),
TimeLockDelta: hopHint.CLTVExpiryDelta,
Flags: direction,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This was indeed not set, but I think it didn't have impact before, because that flag was never read right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it did have impact, i.e. it was an open bug, but the situations where it matters were rare enough such that it wasn't noticed. I haven't fully thought this out. What happened was that when I tried to fix UpdateEdgePolicy to consider direction as well, following your comment, the (new) test failed, and adding direction here fixed it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it was an omission that surfaced when you fixed UpdateEdgePolicy. But I think that before that, the situations where it mattered where non-existent. Just interested to know whether that was true. In any case, it is fixed now.

@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch from a0ccce4 to d469a0e Compare December 11, 2018 10:06
@Bluetegu
Copy link
Contributor Author

Changes made.
The CI cover test failed, but I believe its a bug in the CI test itself (there is an open PR that fixes the coverage test).
Please see inline my response to the review issues.
Thanks!

@btcdanny
Copy link

@halseth Hi Johan, Is the block tag necessary? Let's try to push this to completion. Thanks!

This would be great if possible, as we at Fastbitcoins are encountering the issue this PR addresses.

@halseth
Copy link
Contributor

halseth commented Jan 28, 2019

@Bluetegu Yes, let's do that :) I created a smaller PR that addresses the exact thing that this is blocked by: #2549

@Bluetegu
Copy link
Contributor Author

Ok, great. Let me know if anything is needed on my side.

@Roasbeef Roasbeef added this to the 0.6 milestone Jan 28, 2019
@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch 2 times, most recently from 373efcd to 5868c85 Compare February 10, 2019 09:58
@halseth
Copy link
Contributor

halseth commented Feb 14, 2019

@Bluetegu #2549 is now merged, we can move this forward :)

@halseth halseth removed the blocked label Feb 14, 2019
@Bluetegu
Copy link
Contributor Author

@halseth Thanks!

@wpaulino wpaulino removed this from the 0.6 milestone Mar 25, 2019
Copy link
Contributor

@halseth halseth left a comment

Choose a reason for hiding this comment

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

I think this is close after a rebase! That should let you remove the AssumeChanValid hack now that #2549 is merged.

Again, really good find for the direction pruning bug, and the added test case is 🔥

// before further processing an edge update. Toggel AssumeChannelValid
// to avoid dropping the channel update, as the utxo set query is
// not mocked in this test.
ctx.router.cfg.AssumeChannelValid = true
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 still needed?

// r3: C->E is pruned
// leaving r4 as viable 4th route
//
// This test was added to test direction initialization in NewPaymentSession;
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent test description! 👍

ctx, cleanUp, err := createTestCtxFromGraphInstance(startingBlockHeight,
testGraph)

defer cleanUp()
Copy link
Contributor

Choose a reason for hiding this comment

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

move defer cleanUp() after error check

// before further processing an edge update. Toggel AssumeChannelValid
// to avoid dropping the channel update, as the utxo set query is
// not mocked in this test.
ctx.router.cfg.AssumeChannelValid = true
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed anymore I think

// not mocked in this test.
ctx.router.cfg.AssumeChannelValid = true

// Send payment
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: missing period.

@@ -35,6 +35,51 @@ type paymentSession struct {
preBuiltRoutes []*Route
}

// edgeLocatorOfEdgePolicy returns the edge locator corresponding to
// the channel edge policy
func edgeLocatorOfEdgePolicy(ep *channeldb.ChannelEdgePolicy) *edgeLocator {
Copy link
Contributor

Choose a reason for hiding this comment

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

just return non-pointer value.


// UpdateEdgePolicy updates edge policy of the routing hints kept in the
// payment session.
func (p *paymentSession) UpdateEdgePolicy(el *edgeLocator,
Copy link
Contributor

Choose a reason for hiding this comment

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

also better to take non-pointer for the edgeLocator here.

log.Error(err)
return err
}
if exists {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be removed now that #2549 is merged.


const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
defer cleanUp()
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment wrt cleanup

// before further processing an edge update. Toggel AssumeChannelValid
// to avoid dropping the channel update, as the utxo set query is
// not mocked in this test.
ctx.router.cfg.AssumeChannelValid = true
Copy link
Contributor

Choose a reason for hiding this comment

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

can be removed

High Priority automation moved this from Final Testing -- Ready For Merge to Needs review Mar 28, 2019
and its parser.

Required to correctly sign and later verify error messages in
some test scenarios.
The simulated error returned was rejected due to signature failure,
and didn't simulate correctly the insufficient fees error as
intended. Fix error by including correct signature.
Hints are updated if the error message is either:
 - FailFeeInsufficient
 - FailAmountBelowMinimum
 - FailIncorrectCltvExpiry
@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch from 5868c85 to 0f1d233 Compare April 6, 2019 18:26
@Bluetegu Bluetegu force-pushed the retry-upon-onion-error-private-2066 branch from 0f1d233 to 3d2cea0 Compare April 6, 2019 18:37
@Bluetegu
Copy link
Contributor Author

Bluetegu commented Apr 6, 2019

I did the rebase, but I cannot figure out why the new direction unitest TestSendPaymentPrivateEdgeDirection fails. It didn't fail in the version before the rebase.
I started a new position and unfortunately I no longer have time to work on LND. I tried to fix this issue the last two weekends without success.
I uploaded my latest attempt. The last commit include all the unfinished code.
I hope someone can continue and complete this work (as well as the other of my open PRs), as it contains fixes for two bugs.
I'll do my best to answer questions etc., and can do some work in weekend, but my bandwidth is very limited now.

@halseth
Copy link
Contributor

halseth commented Apr 8, 2019

@Bluetegu No worries! Thanks for your excellent contributions 👍

@halseth
Copy link
Contributor

halseth commented Jul 17, 2020

Somebody should pick this up and rebase the still relevant parts.

@Roasbeef
Copy link
Member

Replaced by #5332

@Roasbeef Roasbeef closed this Jun 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
first pass review done PR has had first pass of review, needs more tho help wanted needs rebase PR has merge conflicts needs testing PR hasn't yet been actively tested on testnet/mainnet P3 might get fixed, nice to have payments Related to invoices/payments routing
Projects
No open projects
High Priority
  
Needs review
Development

Successfully merging this pull request may close these issues.

None yet

7 participants