session+accounts: add RPC server

This commit is contained in:
Oliver Gugger 2022-11-17 16:50:48 +01:00
parent 6af85cfb70
commit cd741091be
No known key found for this signature in database
GPG key ID: 8E4256593F177720
4 changed files with 259 additions and 6 deletions

View file

@ -9,6 +9,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"gopkg.in/macaroon-bakery.v2/bakery"
)
const (
@ -148,6 +149,31 @@ var (
// interceptor.
ErrNotSupportedWithAccounts = errors.New("this RPC call is not " +
"supported with restricted account macaroons")
// MacaroonPermissions are the permissions required for an account
// macaroon.
MacaroonPermissions = []bakery.Op{{
Entity: "info",
Action: "read",
}, {
Entity: "offchain",
Action: "read",
}, {
Entity: "offchain",
Action: "write",
}, {
Entity: "onchain",
Action: "read",
}, {
Entity: "invoices",
Action: "read",
}, {
Entity: "invoices",
Action: "write",
}, {
Entity: "peers",
Action: "read",
}}
)
// Store is the main account store interface.

223
accounts/rpcserver.go Normal file
View file

@ -0,0 +1,223 @@
package accounts
import (
"context"
"encoding/hex"
"fmt"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
"gopkg.in/macaroon.v2"
)
// RPCServer is the main server that implements the Accounts gRPC service.
type RPCServer struct {
litrpc.UnimplementedAccountsServer
service *InterceptorService
superMacBaker session.MacaroonBaker
}
// NewRPCServer returns a new RPC server for the given service.
func NewRPCServer(service *InterceptorService,
superMacBaker session.MacaroonBaker) *RPCServer {
return &RPCServer{
service: service,
superMacBaker: superMacBaker,
}
}
// CreateAccount adds an entry to the account database. This entry represents
// an amount of satoshis (account balance) that can be spent using off-chain
// transactions (e.g. paying invoices).
//
// Macaroons can be created to be locked to an account. This makes sure that
// the bearer of the macaroon can only spend at most that amount of satoshis
// through the daemon that has issued the macaroon.
//
// Accounts only assert a maximum amount spendable. Having a certain account
// balance does not guarantee that the node has the channel liquidity to
// actually spend that amount.
func (s *RPCServer) CreateAccount(ctx context.Context,
req *litrpc.CreateAccountRequest) (*litrpc.CreateAccountResponse,
error) {
log.Infof("[createaccount] balance=%d, expiration=%d",
req.AccountBalance, req.ExpirationDate)
var (
balanceMsat lnwire.MilliSatoshi
expirationDate time.Time
)
// If the expiration date was set, parse it as a unix time stamp.
// Otherwise, we leave it nil to indicate the account has no expiration
// date.
if req.ExpirationDate > 0 {
expirationDate = time.Unix(req.ExpirationDate, 0)
}
// Convert from satoshis to millisatoshis for storage.
balance := btcutil.Amount(req.AccountBalance)
balanceMsat = lnwire.NewMSatFromSatoshis(balance)
// Create the actual account in the macaroon account store.
account, err := s.service.NewAccount(balanceMsat, expirationDate)
if err != nil {
return nil, fmt.Errorf("unable to create account: %v", err)
}
var rootKeyIdSuffix [4]byte
copy(rootKeyIdSuffix[:], account.ID[0:4])
macRootKey := session.NewSuperMacaroonRootKeyID(rootKeyIdSuffix)
accountCaveat := checkers.Condition(
macaroons.CondLndCustom,
fmt.Sprintf("%s %x", CondAccount, account.ID[:]),
)
macHex, err := s.superMacBaker(ctx, macRootKey, &session.MacaroonRecipe{
Permissions: MacaroonPermissions,
Caveats: []macaroon.Caveat{{
Id: []byte(accountCaveat),
}},
})
if err != nil {
return nil, fmt.Errorf("error baking account macaroon: %v", err)
}
macBytes, err := hex.DecodeString(macHex)
if err != nil {
return nil, fmt.Errorf("error decoding account macaroon: %v",
err)
}
return &litrpc.CreateAccountResponse{
Account: marshalAccount(account),
Macaroon: macBytes,
}, nil
}
// UpdateAccount updates an existing account in the account database.
func (s *RPCServer) UpdateAccount(_ context.Context,
req *litrpc.UpdateAccountRequest) (*litrpc.Account, error) {
log.Infof("[updateaccount] id=%s, balance=%d, expiration=%d", req.Id,
req.AccountBalance, req.ExpirationDate)
// Account ID is always a hex string, convert it to our account ID type.
var accountID AccountID
decoded, err := hex.DecodeString(req.Id)
if err != nil {
return nil, fmt.Errorf("error decoding account ID: %v", err)
}
copy(accountID[:], decoded)
// Ask the service to update the account.
account, err := s.service.UpdateAccount(
accountID, req.AccountBalance, req.ExpirationDate,
)
if err != nil {
return nil, err
}
return marshalAccount(account), nil
}
// ListAccounts returns all accounts that are currently stored in the account
// database.
func (s *RPCServer) ListAccounts(context.Context,
*litrpc.ListAccountsRequest) (*litrpc.ListAccountsResponse, error) {
log.Info("[listaccounts]")
// Retrieve all accounts from the macaroon account store.
accts, err := s.service.Accounts()
if err != nil {
return nil, fmt.Errorf("unable to list accounts: %v", err)
}
// Map the response into the proper response type and return it.
rpcAccounts := make([]*litrpc.Account, len(accts))
for i, acct := range accts {
acct := acct
rpcAccounts[i] = marshalAccount(acct)
}
return &litrpc.ListAccountsResponse{
Accounts: rpcAccounts,
}, nil
}
// RemoveAccount removes the given account from the account database.
func (s *RPCServer) RemoveAccount(_ context.Context,
req *litrpc.RemoveAccountRequest) (*litrpc.RemoveAccountResponse,
error) {
log.Infof("[removeaccount] id=%v", req.Id)
// Account ID is always a hex string, convert it to our account ID type.
var accountID AccountID
decoded, err := hex.DecodeString(req.Id)
if err != nil {
return nil, fmt.Errorf("error decoding account ID: %v", err)
}
copy(accountID[:], decoded)
// Now remove the account.
err = s.service.RemoveAccount(accountID)
if err != nil {
return nil, fmt.Errorf("error removing account: %v", err)
}
return &litrpc.RemoveAccountResponse{}, nil
}
// marshalAccount converts an account into its RPC counterpart.
func marshalAccount(acct *OffChainBalanceAccount) *litrpc.Account {
rpcAccount := &litrpc.Account{
Id: hex.EncodeToString(acct.ID[:]),
InitialBalance: uint64(acct.InitialBalance.ToSatoshis()),
CurrentBalance: acct.CurrentBalanceSats(),
LastUpdate: acct.LastUpdate.Unix(),
ExpirationDate: int64(0),
Invoices: make(
[]*litrpc.AccountInvoice, 0, len(acct.Invoices),
),
Payments: make(
[]*litrpc.AccountPayment, 0, len(acct.Payments),
),
}
for hash := range acct.Invoices {
i := &litrpc.AccountInvoice{
Hash: make([]byte, lntypes.HashSize),
}
copy(i.Hash, hash[:])
rpcAccount.Invoices = append(rpcAccount.Invoices, i)
}
for hash, paymentEntry := range acct.Payments {
p := &litrpc.AccountPayment{
Hash: make([]byte, lntypes.HashSize),
State: paymentEntry.Status.String(),
FullAmount: int64(paymentEntry.FullAmount.ToSatoshis()),
}
copy(p.Hash, hash[:])
rpcAccount.Payments = append(rpcAccount.Payments, p)
}
if !acct.ExpirationDate.IsZero() {
rpcAccount.ExpirationDate = acct.ExpirationDate.Unix()
}
return rpcAccount
}

View file

@ -1,6 +1,7 @@
package session
import (
"context"
"fmt"
"time"
@ -54,6 +55,10 @@ type Session struct {
RemotePublicKey *btcec.PublicKey
}
// MacaroonBaker is a function type for baking a super macaroon.
type MacaroonBaker func(ctx context.Context, rootKeyID uint64,
recipe *MacaroonRecipe) (string, error)
// NewSession creates a new session with the given user-defined parameters.
func NewSession(label string, typ Type, expiry time.Time, serverAddr string,
devServer bool, perms []bakery.Op, caveats []macaroon.Caveat) (*Session,

View file

@ -43,12 +43,11 @@ type sessionRpcServer struct {
// sessionRpcServerConfig holds the values used to configure the
// sessionRpcServer.
type sessionRpcServerConfig struct {
basicAuth string
dbDir string
grpcOptions []grpc.ServerOption
registerGrpcServers func(server *grpc.Server)
superMacBaker func(ctx context.Context, rootKeyID uint64,
recipe *session.MacaroonRecipe) (string, error)
basicAuth string
dbDir string
grpcOptions []grpc.ServerOption
registerGrpcServers func(server *grpc.Server)
superMacBaker session.MacaroonBaker
firstConnectionDeadline time.Duration
permMgr *perms.Manager
}