-
Notifications
You must be signed in to change notification settings - Fork 945
paymod: Payments reimagined (part 01) #3753
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
Conversation
c009909
to
8bb9edc
Compare
The modifier design is really nice and modular! very cool 😎 |
20aa3fb
to
71696c9
Compare
One thing I have been thinking of is to separate pathfinding into a separate module. What I mean is, there is a module within Periodically the pay logic would inform the pathfinding module that some node/channel failed a payment of XX amount, and the pathfinding module would rebox routes that include that channel / node into lower-amount boxes. This allows us to amortize the time we are waiting for HTLCs to be resolved or failed into (speculative) pathfnding, and there may be more efficient pathfinding algorithms that can generate multiple paths in a single run rather than just a single path, So roughly, the interface between the pay logic and the pathfinder module would be:
This seems vastly different from your plan, though. In any case, it would be possible to expose the above proposed pathfinding module as a set of commands provided by a built-in plugin (separate from |
That is indeed already possible with the payment modifiers as implemented in this PR. In fact just yesterday I implemented the first modifier that uses its own logic to compute a route, completely sidestepping the call to The logic to compute the route can be arbitrary complex, can rely on other plugins or RPC calls, and can do arbitrary choices, and report metrics back to whatever mechanism it is using. And best of all we don't have to define yet another interface. The idea behind the new payment flow is that it provides the bare minimum to get a successful payment through in the ideal case, but still allows enough flexibility to override/replace whatever doesn't work, and add reactions to deviations from the happy path. |
34efe7e
to
dd4e95f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack a61bf50
Minor feedback only..
p->step = PAYMENT_STEP_RETRY; | ||
} | ||
|
||
payment_continue(p); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean about payment_continue() being easy to miss.
Perhaps this function should simply return, and the "payment_continue" should be in the caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see payment_continue gets called from other callbacks, making this more difficult. OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's pretty similar to the command_still_pending
/command_fail
/command_success
construction, so I though it'd be nice to build something similar to that. I can do that on top of paymod-03
if you agree that it's a good idea.
plugins/libplugin-pay.c
Outdated
if (p->parent == NULL && cmd == NULL) { | ||
/* This is the tree root, but we already reported success or | ||
* failure, so noop. */ | ||
return command_still_pending(NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yech, that's ugly! I'm not convinced payment_finished should retrurn command_result...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason it is like this is that payment_finished
encapsulates the logic to determine whether a command is pending or not. It is the place where we call command_fail
and command_finished
. Those two functions are marked as warn_unused_result
, so I thought it easiest to hand them up and then silence the result.
We can of course silence it by doing something else, but I have no good ideas on how to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some more googling it seems like we can opt-out of the warn_unused_result
using a cast to void (???):
(void)command_finished(cmd, ret);
Still not nice, but I added what they'd look like in the last few fixup!
-commits (they clash with other things so I haven't reordered them just yet).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, gcc
(7.5) doesn't seem to like this trick, and still warns about the unused result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plugins/libplugin-pay.c
Outdated
cur->tlv_payload = tlv_tlv_payload_new(cr->hops); | ||
tlvstream_set_tu64( | ||
&cur->tlv_payload->fields, | ||
TLV_TLV_PAYLOAD_AMT_TO_FORWARD, | ||
p->route[i + 1].amount.millisatoshis); /* Raw: TLV payload generation*/ | ||
tlvstream_set_tu32(&cur->tlv_payload->fields, | ||
TLV_TLV_PAYLOAD_OUTGOING_CLTV_VALUE, | ||
p->start_block + | ||
p->route[i + 1].delay); | ||
tlvstream_set_short_channel_id( | ||
&cur->tlv_payload->fields, | ||
TLV_TLV_PAYLOAD_SHORT_CHANNEL_ID, | ||
&p->route[i + 1].channel_id); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we generate this? This is very unpretty... :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could move it into a separate function for starters, not truly generated, but should clean this up nicely.
I pushed an update that attempts to fixup all the feedback. Here's the range-diff, no non-fixup commit was touched, so reviewing the fixups should be sufficient. Not squashing just yet, since there is the "return command_result" change that needs to be acknowledged before I start working on the conflicts that reordering causes :-) Ping @rustyrussell :-) |
Autosquashed and rebased on top of |
Rebase on top of
|
The actual steps are mocked out, but we can already run through the various phases of a payment, and the modifiers should be called for each state.
This commit can be reverted/skipped once we have implemented all the logic and have feature parity with the normal `pay`. It's main purpose is to expose the unfinished functionality to test it, without completely breaking the existing `pay` command.
This should make it easy for JSON-RPC functions and modifiers to get the associated data for a given modifier name. Useful if a modifier needs to access its parent's modifier data, or in other functions that need to access modifier data, e.g., when passing destination pointers into the `param()` call.
This is likely a bit of overkill for this type of functionality, but it is a nice first use-case of how functionality can be compartmentalized into modifiers. If makes swapping retry mechanisms in and out really simple.
A payment is considered finished if it is in a final state (success or failure) or all its sub-payments are finished. If that's the case we notify `payment_finished` and bubble up through `payment_child_finished`, eventually bubbling up to the root, which can then report success of failure back to the RPC command that initiated the whole process.
This is necessary so we can build the absolute locktimes in the next step. For now just fetch the blockheight on each (sub-)payment, later we can reuse the root blockheight if it ends up using too much traffic.
Te `sendonion` docs where claiming that the `first_hop` needs to specify a `channel_id` whereas it should really be the `node_id` of the peer we are trying to contact.
This is just for testing for now, TLV payload computation will come next. We stage all the payloads in deserialized form so modifiers can modify them more easily and serialize them only before actually calling `createonion`.
After we gave each modifier a chance to have its say, we can now proceed to generate the onion that's going to be sent in the next step.
This is necessary so we can later aggregate across an entire tree of attempts and report success or failure to the RPC caller.
The status of what started as a simple JSON-RPC call is now spread across an entire tree of partial payments and payment attempts. So we collect the status in a single struct in order to report back success of failure.
We were just handwaving the partid generation, which broke some tests that expected the first payment attempt to always have partid=0, so here we just track the partids assigned in the payment tree, starting at 0.
We need to keep them around so we can inspect them later. We'll also need a background cleanup every once in a while to free some memory. More on that in a future commit.
We were passing the `cmd` instance around where `plugin` suffices (used to start RPC calls and logs).
Trying to rework the TLV streams to have a more homogenous interface to work with. This is by no means a complete implementation, just the groundwork that is going to be used by the wire code generator to generate the specific accessors, but it's enough so we can manipulate TLV streams in the onion and later just switch to the generated ones.
Suggested-by: Rusty Russell <@rustyrussell>
Ack 3412a69 |
This is only a partial implementation of the new payment flow, but given the
looming release deadline and the sheer size of the change, I thought it's best
to get some code reviews in now
Design
The current payment flow is a multi-step process in the
pay
plugin (and someothers) that grew organically as logic and more features were added over the
time. This PR is my attempt to reimagine and consolidate the payment flow, to
consolidate some commonalities, better compartmentalize the existing addons,
and facilitate future modifications of the flow.
In order to get to this lofty goal I separated the logic into two separate
parts: the
payment
which consists of the core functionality required toperform a payment, and
payment_modifier
s which have associated data and arecalled at each step of the payment flow to allow them to modify the payment
behavior. The core payment flow consists of the following steps:
After each step is complete all modifiers registered with the flow will be
called, and they are allowed to modify their local data and the data of the
payment in order to fulfill their purpose. In particular they are allowed to
set a payment into any state, including two states (
retry
andsplit
) thatare not part of the core flow. These are indications that the payment failed,
but was retried with different parameters or was split into multiple
sub-payments, e.g., in MPP.
This brings us to the structure of payments: payments and their sub-payments
are organized in a tree, with the inner nodes representing failed attempts
that have been retried in some form, and leafs representing the currently
active or final attempts. The root of the tree is the original attempt and
serves as a coordination point for all sub-payments, aggregating information
that is shared among all attempts such as a list of
channel_hint
s used tocompute exclusions, and the invoice information that initiated the payment
flow.
This structure allows modifiers to implement almost arbitrary logic, while
keeping the core logic clean. Modifiers (both planned and implemented)
include:
final_cltv
: the invoice can specify a final CLTV offset that just adds tothe route CLTV deltas, so calling after
got_route
we can adjust them tomatch.
routehints
: these change the destination for which we callgetroute
andonce in
got_route
we can append the current routehint to form completeroute.
waitblockheight
: if a node reports that our CLTV is too low we can inferthe height we should be at and then wait to reach that height before
re-attempting.
shadow_route
: since this is just an offset applied to the values we cancompute the shadow route and then adjust the amount passed to the
getroute
call.retry
: if a payment fails we can just retry for a fixed number of timeswith different routes.
split
: this is the core functionality of MPP, splitting a payment intomultiple sub-payments if it fails.
keysend
is just adjusting the TLV payload passed tocreateonion
toinclude the preimage.
There are certainly some suboptimal solutions, however I think this structure
can be used to better experiment and implement future extensions rather than
having to weave the functionality into the existing flow (keysend sending
functionality for example would not have any need for the
routehints
modifier, but the rest is shared).
Testing
The functionality is implemented in parallel to the existing
json_pay
infrastructure, but is planned to become a drop-in replacement, allowing us to
eventually remove the old implementation and just use the new one. To test the
new flow you can use
lightning-cli paymod [args]
instead oflightning-cli pay [args]
though some modifiers are not implemented yet, and some are in thenext parts of this PR.
I am testing feature parity by changing
pay.c
to usejson_paymod
insteadof
json_pay
, allowing me to run the existing (unmodified) tests against thenew logic.
The ugly parts:
I'm calling these out because I am currently not addressing them and they
aren't a high priority, but we should clean these up eventually, and they may
cause reviewers to stumble a bit:
payment_continue(p)
to advance the state-machine,and I forgot that a couple of times. Something like we do with
command_still_pending
and friends would be nice.command
instance is ratherstrange, even though it doesn't do much (you can pass
NULL
to it), sincewe now potentially have multiple concurrent RPC calls in flight, if one of
them were to respond to the original
cmd
it'd break things. Making theRPC interface independent of having a
command
instance would be a goodidea.
struct
s to facilitate modifying them, however the functions and structdefinitions are spread all over the place. I'd like to consolidate them
under
common/
eventually, so we can use them on both thelightningd
andthe plugin side, and not have to replicate the extraction of data from JSON
responses all over the place.
Next up
I have most of the functionality implemented, and we're down to 12 failing
tests in my development branch. I'll publish the next part
as soon as it settles down, i.e., doesn't get changed anymore, and I'll defer
cleanups and renaming to a last cleanup part in order not to delay the overall
progress and release.
Given the very new and unproven nature I'd propose we release this for
keysend
sending support directly, but gate the replacement of thepay
command behind either a
!COMPAT_090
or theDEVELOPER
flag. This wouldminimize the impact to the majority of users, but still allow us to collect
experience from
keysend
and users that opted into testing it. However it'dalso mean that
mpp-pay
is a separate command or a developer-only command forthis release.
Changelog-None