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

Add support for custodial off-chain accounts #363

Merged
merged 13 commits into from
Dec 2, 2022
Merged

Add support for custodial off-chain accounts #363

merged 13 commits into from
Dec 2, 2022

Conversation

guggero
Copy link
Member

@guggero guggero commented May 12, 2022

Early preview of the LiT custodial off-chain accounts feature.

Depends on #308.
Depends on #432.

EDIT: All TODOs have been resolved 🎉

@guggero
Copy link
Member Author

guggero commented May 20, 2022

This still has a bunch of TODOs and things that can be improved, that's why it's still in draft. But would be nice if you could take a first look at the general architecture, @ellemouton and @Roasbeef.

@ellemouton
Copy link
Member

yep! currently doing so :)

Copy link
Member

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

still need to do a more in depth pass. But so far this looks amazing 😱 🔥 leaving a few comments so long

accounts/interceptor.go Outdated Show resolved Hide resolved
rpcmiddleware/interface.go Outdated Show resolved Hide resolved
@guggero guggero force-pushed the accounts branch 2 times, most recently from 88804ed to e4e45cd Compare June 21, 2022 11:30
@guggero guggero marked this pull request as ready for review June 21, 2022 11:30
@guggero
Copy link
Member Author

guggero commented Jun 21, 2022

Taking this out of WIP state since this is now a bit better tested and also received its own set of integration tests!

The following TODOs are still in place:

  • add unit tests
  • take in-flight payments into account when calculating remaining account balance
  • add support for creating a custom "account" LNC session by adding custom caveats to the AddSession RPC.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Amazing PR!

Rather large, so I did an initial pass through, focusing on internalizing the porposed architecture. To main things popped into my head:

  1. Why not return the macaroon from the account creation/list calls so the caller can store that along side the account ID?
  2. What if we instead stored the latest account details in the macaroon itself? This would make the service more stateless as the balances would be encoded in the macaroon itself. The persistence gets a bit simpler, but then we need to sort of burden the user with storing that new updated macaroon. Also this wouldn't really work for the invoices unfortunately, since we'd want to keep the user's macaroon somewhat compact.

rpcmiddleware/interface.go Outdated Show resolved Hide resolved
// request and inspects and potentially modifies the response.
func NewResponseRewriter(requestSample proto.Message,
responseSample proto.Message,
typedResponseHandler interface{}) *DefaultChecker {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can dip into a bit of generics usage here? So something like:

type responseHandler[T any]  func(T) error 

Still getting through the diff, so I don't know if it'll use useful, but just a hunch.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried that but failed. Maybe I'll try again after I've completed everything else.

terminal.go Show resolved Hide resolved
accounts/checkers.go Outdated Show resolved Hide resolved
accounts/checkers.go Outdated Show resolved Hide resolved
accounts/store.go Outdated Show resolved Hide resolved
// chargeAccount subtracts the given amount from an account's balance.
func (s *Store) chargeAccount(id AccountID, paymentHash lntypes.Hash,
amount lnwire.MilliSatoshi) error {

Copy link
Member

Choose a reason for hiding this comment

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

I need to get further in the diff, but in order to prevent race conditions, we may need to actually be holding an internal mutex here. Though I think that things are all serial based on my mental model so far.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, a lock was definitely missing. Fixed now.

litrpc/lit-accounts.proto Show resolved Hide resolved
}

message Account {
/*
Copy link
Member

Choose a reason for hiding this comment

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

Should we also return the raw macaroon here? Otherwise how will it be obtained?

One architecture I have in mind is:

  • Service maintains an external DB of the macaroon for each user's accoutn
  • Each time a user wants to do something, the services get the macaroon then issues the response as normal

So the service just needs to handle the mapping from user to account macaroon.

One other alternative architecture would be the discard model: the account service actually encodes the latest balance in the macaroon itself. This minimizes the state, as its the clients jobs to maintain this macaroon, and they can't modify it w/ breaking the HAMC check.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added the baking of an account macaroon to the RPC server and updated the docs.

doc/accounts.md Outdated
This created a new account (ID ``) with an initial balance of 50k satoshis and
no expiration.

### Bake a new macaroon for the user
Copy link
Member

Choose a reason for hiding this comment

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

Related to my comment above: what if we did this for the user?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, great idea. Did that.

Copy link
Member

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

tACK 🔥 Leaving some initial comments :)

rpcmiddleware/interface.go Outdated Show resolved Hide resolved
terminal.go Show resolved Hide resolved
accounts/store.go Outdated Show resolved Hide resolved
accounts/service.go Outdated Show resolved Hide resolved
accounts/interceptor.go Outdated Show resolved Hide resolved
rpcmiddleware/manager.go Outdated Show resolved Hide resolved
accounts/checkers.go Outdated Show resolved Hide resolved
accounts/checkers.go Outdated Show resolved Hide resolved
accounts/checkers.go Show resolved Hide resolved
accounts/store.go Outdated Show resolved Hide resolved
@lightninglabs-deploy
Copy link

@guggero, remember to re-request review from reviewers when ready

@guggero
Copy link
Member Author

guggero commented Aug 5, 2022

!lightninglabs-deploy mute

@guggero
Copy link
Member Author

guggero commented Oct 17, 2022

Yayy, I was finally able to resolve all TODOs 🚀
This PR is now fully ready for final reviews.

@sangaman
Copy link

Yes, on every startup we re-initialize the payment tracking for each payment and invoice update stream.

So this should be pretty robust. Though in general "lnd instance that comes online & offline unpredictably" is never recommended in a production environment. Or are you thinking about a mobile setup? Curious about your use case!

Gotcha, I think one thing I was overlooking was that (if I'm not mistaken) litd will stop when it sees that lnd has stopped. So this Start function will be called every time either litd or lnd restart. My concern was only that lnd goes down, all subscriptions are lost, and then when it comes back up we don't re-establish the streams.

I wasn't thinking about a mobile setup or anything like that, just about what might happen if there are unexpected outages in an otherwise stable environment.

@guggero
Copy link
Member Author

guggero commented Nov 28, 2022

litd will stop when it sees that lnd has stopped

Ah yes, that is important to know in this context. litd is indeed set up to stop (and be re-started by systemd, docker, k8s, whatever) when the connection to lnd is lost.

Copy link

@sangaman sangaman left a comment

Choose a reason for hiding this comment

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

Thanks for taking the time to respond to my feedback, this is great stuff and will be very useful I think.

@sangaman
Copy link

My last handful of thoughts about this PR & feature:

  1. It might be nice to have an RPC call that increments an account's balance. That way there's an easy way to, for example, add 1000 satoshis to an account. Or possibly decrement balance (by passing a negative value), but then there'd need to be the checks for sufficient balance on the account. Right now, you can update an account's balance via RPC, but that's only good if you're trying to set the balance to a specific amount, not add or subtract from the existing balance. Trying to do that by querying the balance and then updating the account balance would be subject to race conditions (maybe the balance updates in between the two calls). I figure this can be done in a follow-up PR, though.

  2. It'd be nice if self-payments from one account to another were better supported. Right now, it seems like it does work if I use the --allow_self_payment flag, which then sends a payment out one channel and back in on the same channel. I think it'd be cool to be able to make an account-to-account payment without actually needing to make a real lightning payment (which may incur a fee, delay, and/or chance of failure). But I can imagine that would be a complicated change since it would likely break some internal assumptions in lnd about payments, namely that they actually involve the external lightning network. Just something to think about for future work.

@guggero
Copy link
Member Author

guggero commented Nov 29, 2022

My last handful of thoughts about this PR & feature:

Thanks a lot for your feedback! I think both of these features make sense for follow-up PRs (given the added complexity for item 2 and the age of this PR, this has been in the works since late 2018 in some form or another). Feel free to add feature request issues to the repo so they don't drop off the radar.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 🌜

Comment on lines +81 to +91
/*
The list of invoices created by the account. An invoice created by an
account will credit the account balance if it is settled.
*/
repeated AccountInvoice invoices = 6;

/*
The list of payments made by the account. A payment made by an account will
debit the account balance if it is settled.
*/
repeated AccountPayment payments = 7;
Copy link

Choose a reason for hiding this comment

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

One last thought when I was looking through this PR, and also probably something that could be addressed in a follow-up PR, but it looks like these include all payments and invoices ever made by the accounts. Therefore when calling ListAccounts, you're fetching all payment and invoice history for all accounts across all time. That seems like it could potentially be a very heavy query.

It might make sense to have a ListAccounts call that just gives you the accounts and balances that exist, and then separate ListInvoices and ListPayments calls that work for a single account specified in the request? Something like that, just so that there's a way to list the accounts without needing to fetch all invoice & payment history.

I can put this in an issue as well, just wanted to raise it here first.

Copy link
Member

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

🔥

With this commit we add a set of utility functions that allow us to
store and retrieve an account in/from a context.
This commit adds the checkers component that is responsible for making
sure all incoming RPC requests and outgoing responses are tracked and
their effects on an account are updated accordingly.
This commit introduces the account interceptor service which makes sure
that each request that contains an account macaroon is intercepted and
passed through the checkers component to track each RPC call's effects
on the account.
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.

None yet

5 participants