-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
lnrpc: holdinvoice design [wip] #2263
Conversation
1960c2e
to
9b51ed6
Compare
lnrpc/invoicerpc/invoice.proto
Outdated
ACCEPTED = 1; | ||
|
||
/** | ||
In the settled state, the invoice isn't payable anymore. We have |
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.
Non-payable settled invoices is a change from the current implementation. Not sure what the use case is for paying to a settled invoice. It may be negative, because business owners can feel obliged to issue refunds? Because it is insecure (preimage already revealed), we may not want to encourage this either? I also try to see the parallel with traditional web checkouts. I don't think it is possible to pay an order twice.
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 think the choice should be left to the receiver if they want to allow receiving multiple times but agree the default should be that they cannot
Related issue: #2208
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.
Shouldn't we then also accept (not yet settle) payments with a value less than the invoice amt?
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.
@cfromknecht What is your idea about these state transitions and when to accept/settle htlcs? I we extend the 'greedy' approach further, we should really lock-in every htlc that we get, even thought the final cltv and/or value may be less than what is specified in the invoice.
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 would say that we should only transition to accept if we have one (or more) HTLCs at that time that we could plausibly settle and meet the invoice requirements.
For single payments: if the cltv or value is less than the invoice requirements, i'd say just reject and not accept.
For multi-path payments:
- if the ctlv is incorrect, fail it
- otherwise,
- if the cltv of any other previous htlcs has expired, fail those
- if the total value hasn't been received, wait
- otherwise, if the total value has been received, accept
(just a rough sketch)
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.
Okay, that is for the OPEN
state. What shall we do in the ACCEPT
and SETTLED
states? Do we go into catch all mode?
lnrpc/invoicerpc/invoice.proto
Outdated
weren't connected to the streaming RPC. | ||
*/ | ||
uint64 add_index = 1; |
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.
why not keep this, and call it last_invoice_number
? that way clients can be sure they're seeing all created invoices
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.
With update_index
they can do exactly do same, because the creation is an event as well?
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 was thinking they would be separate for backwards compatibility. We can have a migration that copies over the existing settle_index entries into the update index, and client indices would be compatible.
If adds are also in the update index, they must have an index that precedes the settle index which breaks old values.
May this isn't so bad, and people just reindex?
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.
So the migration would be to create update records as follows?
update index: settle index
invoice_number: add index
state: settled
settle_date: settle_date
amt_paid: amt_paid
I think it breaks an assumption anyway in that from the migration onwards not every value of update_index will be a settle event anymore.
What I don't like about two indices is that you need to call SubscribeInvoices twice to get a complete picture of the current state.
I think I am in favor of just leaving settle and add index what they are - we need to do that anyway because we are only adding a subserver - and add an updateindex to the db that includes the created event as well. For the migration, we can first insert all the existing adds in the updateindex and then all existing settles.
@@ -244,28 +352,34 @@ message ListInvoiceResponse { | |||
message InvoiceSubscription { | |||
/** | |||
If specified (non-zero), then we'll first start by sending out | |||
notifications for all added indexes with an add_index greater than this | |||
value. This allows callers to catch up on any events they missed while they | |||
all invoices that have an last_update_index greater than this value. This allows |
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.
Another option here is to replay the entire event stream since the last_update_index, instead of skipping to the aggregate of multiple updates to the same invoice. Of the two, I have a slight preference for replaying the event-stream since:
- client handling is simpler, updates always contiguous, no initial-startup handling for jumping ahead in stream
- doesn't require a scan of the entire db for each new client (would need to decode+check O(n) invoices, even if client only missed a few updates)
OTOH, the log-based approach takes up slightly more storage by keeping all of the events and not-aggregating the final values
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.
doesn't require a scan of the entire db for each new client (would need to decode+check O(n) invoices, even if client only missed a few updates)
this isn't necessary, because of the update_index
bucket, which is sorted? it looks up the update_index
specified by the subscribeinvoices
client and then uses a cursor to go through all the changed invoices until the end of the bucket is reached.
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.
Ahh okay, I was thinking the invoices themselves would be mutated, and we'd just search them all for ones that contain last_update_index >= last_client_update_index
. Is your thinking we'd store a complete invoice for each update_index
?
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.
From your comments above, would we garbage collect a prior update_index
for an invoice that gets updated?
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.
Okay, I think i've got a picture of what you're describing. The update_index
only stores the invoice_number
, the invoice itself gets mutated directly and we can use the update_index
to scan for any invoices that have been recently modified.
Indeed I like this and agree it's perfectly feasible. From an API perspsective it seems the only real tradeoff between the two is whether we expose intermediate state transitions to the client.
I wonder how much more complex the client state machine would have to be to handle these gaps. Say the invoice goes through states A -> B -> C -> D. If all intermediate transitions are preserved, the client only needs to handle A -> B, B -> C, and C -> D since they'll be replayed reliably.
If we skip intermediate states, the client also has to be ready to handle A -> C, A -> D, and B -> D, which definitely increases number of transitions that need to implemented and tested.
There is also information lost via aggregation, e.g. the client can't distinguish between the OPEN -> CANCEL and OPEN -> ACCEPTED -> CANCELED if they only see the first and last states.
I don't know how many applications will require those intermediate states, but that info sounds useful at the same time :)
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.
If we choose to have different semantics via rpc vs internal subscriptions, I don't think this poses too much overhead. The mechanism you proposed would work the same, just ignoring the additional fields in the value (since the invoice has already been updated to its final state).
It could be a simple boolean option internally (possibly exposed via rpc as well) that asks for intermediate states on restart
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 was just considering the trade-off between implementation effort and functionality. I agree that deterministic behaviour is better. What is an RSM btw?
You are right about the abstraction level. These are two independent things.
And yes, the diff in definition of update_index
isn't much. It is a new bucket anyway.
Okay, so we go for replayable history.
Just wondering if we want to keep the dynamic fields in the invoice as well. The alternative is only store static fields in the invoice and use the invoice field update_index
to retrieve the latest dynamic values.
In any case, I think if we store intermediate values, we should also stream them over rpc.
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 added a commit for the InvoiceEvent
message.
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.
Catching up on this conversation, I think this is similar to what I described above, in that the API can be purely event based so the user doesn't need to filter out fields which each update, instead we give them a discrete event to handle.
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.
Filter out, as in ignore all static invoice fields because they are already known?
lnrpc/invoicerpc/invoice.proto
Outdated
notifying the client of invoice updates. The caller can optionally specify | ||
a last_update_index to catch up with missed updates. | ||
*/ | ||
rpc SubscribeInvoices (InvoiceSubscription) returns (stream Invoice) {} |
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.
Instead of this global subscribe function (is it scalable?), we could also go for a SubscribeInvoice with a specific invoice number. Isn't it that applications (except for some kind of global node monitoring dashboard), know their own invoices?
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.
Then we also wouldn't need all those indices. Just start the stream by sending back the current invoice state and keep sending updates as they happen. It will be a never ending stream in case we decide to keep accepting htlcs even though the invoice is settled. But when the client isn't interested anymore, it can just close the stream.
This SubscribeInvoice
can even be combined with AddInvoice
. Create an idempotent AddInvoice
.
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.
Agreed! It would certainly be useful to also be able to filter for a specific invoice (or maybe even a list?)
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.
How do you feel about an idempotent, streaming AddInvoice
? It can still be used like the current AddInvoice
. Just close the stream after the first (creation) update is received.
Calling AddInvoice
again, will stream again all events from the beginning.
How could this be done efficiently? Retrieving all updates for an invoice without scanning through the full update_index
bucket? Maybe we need a sub-bucket in the invoice containing all update_indices? Then it feels maybe safer to also store the dynamic values in that bucket. All invoice data close to each other. A different index is then needed to do the global SubscribeInvoices
. This can again be just update_index -> invoice_number
.
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.
Another question that I have with the existing global SubscribeInvoices
is when to store those last indices? Updating a persistent store after every event doesn't sound good. But using a timer is also not so clean.
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.
SubscribeInvoices
is nice for a global monitoring dashboard, but I suspect that most other applications know their own invoices and are much better served with a per invoice subscription.
9b51ed6
to
61256ce
Compare
Do we also need to think about the on-chain invoice settle path? (contract resolver settling invoice) |
Seems we wouldn't need to special case this at all? The resolver would just call the same method as the link does when it goes to settle an incoming invoice afaict. |
flag to false. If none of the parameters are specified, then the last 100 | ||
invoices will be returned. | ||
*/ | ||
rpc ListInvoices (ListInvoiceRequest) returns (ListInvoiceResponse) {} |
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.
In our current case, do we have any need for stuff like reversal?
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.
For my use, I don't need to list invoices at all. Keep track of the hashes myself.
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.
This is in the proto to have a complete proposal for a new invoice interface. Once that stabilizes it should be able to replace what we currently have in the main rpc. I am leaving the call in to prevent us coming up with a system that doesn't take it into account.
duplicated invoices are rejected, therefore all invoices *must* have a | ||
unique payment preimage. | ||
*/ | ||
rpc AddInvoice (AddInvoiceRequest) returns (AddInvoiceResponse) {} |
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.
Perhaps we should introduce a distinct call for hodl invoices to make the difference more clear on the RPC interface?
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.
Separate call vs a field in the request, not much difference to me. The hold
field will have a default of false, so unaware users won't be bothered.
InvoiceState state = 22; | ||
} | ||
|
||
message InvoiceEvent { |
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.
Perhaps we should use a oneof
here to create a sort of sum-type? So for example, an InvoiceEvent
is either a:
AddEvent
AcceptEvent
SettleEvent
CancelEvent
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.
This way the client doesn't need to know which fields will be set and which one won't be when we send an update. Instead, they just need to be aware of each event type, and we can add additional ones in the future.
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 think we want that state type anyway to provide full info on list/lookup invoice(s). Also oneof isn't a prototype of beauty in go.
} | ||
|
||
|
||
enum InvoiceState { |
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.
See the comment above about the possibility of adding an event type using a oneof
. This might cause us to meld this state into the events themselves. It may even be the case that when we list or query for an invoice, then we show all the individual events in the response, and a final set of fields that summarizes the final state.
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.
But that final set probably contains a state field again?
lnrpc/invoicerpc/invoice.proto
Outdated
A list of invoices from the time slice of the time series specified in the | ||
request. | ||
*/ | ||
repeated Invoice invoices = 1; |
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.
See comment re including the entire event stream for each invoice and the final state. Can also be helpful to know when an invoice was settled, and how long it took for it to be settled, etc. This would mean we can remove things like settled_date
from the invoice as it's in the main event stream.
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.
We need to discuss whether we always want to return the full history. For now I did an intermediate step. Updated the proto to return the last version of the dynamic data in an invoice.
@@ -244,28 +352,34 @@ message ListInvoiceResponse { | |||
message InvoiceSubscription { | |||
/** | |||
If specified (non-zero), then we'll first start by sending out | |||
notifications for all added indexes with an add_index greater than this | |||
value. This allows callers to catch up on any events they missed while they | |||
all invoices that have an last_update_index greater than this value. This allows |
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.
Catching up on this conversation, I think this is similar to what I described above, in that the API can be purely event based so the user doesn't need to filter out fields which each update, instead we give them a discrete event to handle.
e9c6034
to
03f589b
Compare
03f589b
to
4d478aa
Compare
Superseded by #2022. |
Decisions:
CancelInvoice
andSettleInvoice
use the standard grpc error value for their result.add_index
toinvoice_number
for less impl. oriented namingTo decide:
ACCEPTED
andSETTLED
?accept_date
and how to deal with state transitions OPEN -> ACCEPT -> OPEN -> ACCEPT?settle_index
bucket?