pool/rpcserver.go
2020-04-03 21:50:22 +02:00

680 lines
18 KiB
Go

package client
import (
"context"
"encoding/hex"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightninglabs/agora/client/account"
"github.com/lightninglabs/agora/client/auctioneer"
"github.com/lightninglabs/agora/client/clientdb"
"github.com/lightninglabs/agora/client/clmrpc"
"github.com/lightninglabs/agora/client/order"
"github.com/lightninglabs/loop/lndclient"
)
const (
// getInfoTimeout is the maximum time we allow for the initial getInfo
// call to the connected lnd node.
getInfoTimeout = 5 * time.Second
)
// rpcServer implements the gRPC server on the client side and answers RPC calls
// from an end user client program like the command line interface.
type rpcServer struct {
started uint32 // To be used atomically.
stopped uint32 // To be used atomically.
// bestHeight is the best known height of the main chain. This MUST be
// used atomically.
bestHeight uint32
server *Server
lndServices *lndclient.LndServices
auctioneer *auctioneer.Client
db *clientdb.DB
accountManager *account.Manager
orderManager *order.Manager
quit chan struct{}
wg sync.WaitGroup
blockNtfnCancel func()
}
// newRPCServer creates a new client-side RPC server that uses the given
// connection to the trader's lnd node and the auction server. A client side
// database is created in `serverDir` if it does not yet exist.
func newRPCServer(server *Server, serverDir string) (*rpcServer, error) {
db, err := clientdb.New(serverDir)
if err != nil {
return nil, err
}
lnd := &server.lndServices.LndServices
return &rpcServer{
server: server,
lndServices: lnd,
auctioneer: server.AuctioneerClient,
db: db,
accountManager: account.NewManager(&account.ManagerConfig{
Store: db,
Auctioneer: server.AuctioneerClient,
Wallet: lnd.WalletKit,
Signer: lnd.Signer,
ChainNotifier: lnd.ChainNotifier,
TxSource: lnd.Client,
}),
orderManager: order.NewManager(&order.ManagerConfig{
Store: db,
AcctStore: db,
Lightning: lnd.Client,
Wallet: lnd.WalletKit,
Signer: lnd.Signer,
}),
quit: make(chan struct{}),
}, nil
}
// Start starts the rpcServer, making it ready to accept incoming requests.
func (s *rpcServer) Start() error {
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
return nil
}
log.Infof("Starting trader server")
ctx := context.Background()
lndCtx, lndCancel := context.WithTimeout(ctx, getInfoTimeout)
defer lndCancel()
info, err := s.lndServices.Client.GetInfo(lndCtx)
if err != nil {
return fmt.Errorf("error in GetInfo: %v", err)
}
log.Infof("Connected to lnd node %v with pubkey %v", info.Alias,
hex.EncodeToString(info.IdentityPubkey[:]))
var blockCtx context.Context
blockCtx, s.blockNtfnCancel = context.WithCancel(ctx)
chainNotifier := s.lndServices.ChainNotifier
blockChan, blockErrChan, err := chainNotifier.RegisterBlockEpochNtfn(
blockCtx,
)
if err != nil {
return err
}
var height int32
select {
case height = <-blockChan:
case err := <-blockErrChan:
return fmt.Errorf("unable to receive first block "+
"notification: %v", err)
case <-ctx.Done():
return nil
}
s.updateHeight(height)
// Start the auctioneer client first to establish a connection.
if err := s.auctioneer.Start(); err != nil {
return fmt.Errorf("unable to start auctioneer client: %v", err)
}
// Start managers.
if err := s.accountManager.Start(); err != nil {
return fmt.Errorf("unable to start account manager: %v", err)
}
if err := s.orderManager.Start(); err != nil {
return fmt.Errorf("unable to start order manager: %v", err)
}
s.wg.Add(1)
go s.serverHandler(blockChan, blockErrChan)
log.Infof("Trader server is now active")
return nil
}
// Stop stops the server.
func (s *rpcServer) Stop() error {
if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) {
return nil
}
log.Info("Trader server stopping")
s.accountManager.Stop()
s.orderManager.Stop()
err := s.db.Close()
if err != nil {
log.Errorf("Error closing DB: %v")
}
err = s.auctioneer.Stop()
if err != nil {
log.Errorf("Error closing server stream: %v")
}
close(s.quit)
s.wg.Wait()
s.blockNtfnCancel()
log.Info("Stopped trader server")
return nil
}
// serverHandler is the main event loop of the server.
func (s *rpcServer) serverHandler(blockChan chan int32, blockErrChan chan error) {
defer s.wg.Done()
for {
select {
case msg := <-s.auctioneer.FromServerChan:
// An empty message means the client is shutting down.
if msg == nil {
continue
}
log.Debugf("Received message from the server: %v", msg)
err := s.handleServerMessage(msg)
if err != nil {
log.Errorf("Error handling server message: %v",
err)
err := s.server.Stop()
if err != nil {
log.Errorf("Error shutting down: %v",
err)
}
}
case err := <-s.auctioneer.StreamErrChan:
// If the server is shutting down, then the client has
// already scheduled a restart. We only need to handle
// other errors here.
if err != nil && err != auctioneer.ErrServerShutdown {
log.Errorf("Error in server stream: %v", err)
err := s.auctioneer.HandleServerShutdown(err)
if err != nil {
log.Errorf("Error closing stream: %v",
err)
}
}
case height := <-blockChan:
log.Infof("Received new block notification: height=%v",
height)
s.updateHeight(height)
case err := <-blockErrChan:
if err != nil {
log.Errorf("Unable to receive block "+
"notification: %v", err)
err := s.server.Stop()
if err != nil {
log.Errorf("Error shutting down: %v",
err)
}
}
// In case the server is shutting down.
case <-s.quit:
return
}
}
}
func (s *rpcServer) updateHeight(height int32) {
// Store height atomically so the incoming request handler can access it
// without locking.
atomic.StoreUint32(&s.bestHeight, uint32(height))
}
// handleServerMessage reads a gRPC message received in the stream from the
// auctioneer server and passes it to the correct manager.
func (s *rpcServer) handleServerMessage(rpcMsg *clmrpc.ServerAuctionMessage) error {
switch msg := rpcMsg.Msg.(type) {
// A new batch has been assembled with some of our orders.
case *clmrpc.ServerAuctionMessage_Prepare:
// Parse and formally validate what we got from the server.
log.Tracef("Received prepare msg from server, batch_id=%x: %v",
msg.Prepare.BatchId, spew.Sdump(msg))
batch, err := order.ParseRPCBatch(msg.Prepare)
if err != nil {
return fmt.Errorf("error parsing RPC batch: %v", err)
}
// Do an in-depth verification of the batch.
err = s.orderManager.OrderMatchValidate(batch)
if err != nil {
// We can't accept the batch, something went wrong.
log.Errorf("Error validating batch: %v", err)
return s.sendRejectBatch(batch, err)
}
// Accept the match now.
//
// TODO(guggero): Give user the option to bail out of any order
// up to this point?
err = s.sendAcceptBatch(batch)
if err != nil {
log.Errorf("Error sending accept msg: %v", err)
return s.sendRejectBatch(batch, err)
}
// We were able to accept the batch. Inform the auctioneer, then
// start negotiating with the remote peers. We'll sign once all
// channel partners have responded.
err = s.server.BatchChannelSetup(batch)
if err != nil {
log.Errorf("Error setting up channels: %v", err)
return s.sendRejectBatch(batch, err)
}
// Sign for the accounts in the batch.
sigs, err := s.orderManager.BatchSign()
if err != nil {
log.Errorf("Error signing batch: %v", err)
return s.sendRejectBatch(batch, err)
}
err = s.sendSignBatch(batch, sigs)
if err != nil {
log.Errorf("Error sending sign msg: %v", err)
return s.sendRejectBatch(batch, err)
}
// The previously prepared batch has been executed and we can finalize
// it by opening the channel and persisting the account and order diffs.
case *clmrpc.ServerAuctionMessage_Finalize:
log.Tracef("Received finalize msg from server, batch_id=%x: %v",
msg.Finalize.BatchId, spew.Sdump(msg))
var batchID order.BatchID
copy(batchID[:], msg.Finalize.BatchId)
return s.orderManager.BatchFinalize(batchID)
default:
return fmt.Errorf("unknown server message: %v", msg)
}
return nil
}
func (s *rpcServer) InitAccount(ctx context.Context,
req *clmrpc.InitAccountRequest) (*clmrpc.Account, error) {
account, err := s.accountManager.InitAccount(
ctx, btcutil.Amount(req.AccountValue), req.AccountExpiry,
atomic.LoadUint32(&s.bestHeight),
)
if err != nil {
return nil, err
}
return marshallAccount(account)
}
func (s *rpcServer) ListAccounts(ctx context.Context,
req *clmrpc.ListAccountsRequest) (*clmrpc.ListAccountsResponse, error) {
accounts, err := s.db.Accounts()
if err != nil {
return nil, err
}
rpcAccounts := make([]*clmrpc.Account, 0, len(accounts))
for _, account := range accounts {
rpcAccount, err := marshallAccount(account)
if err != nil {
return nil, err
}
rpcAccounts = append(rpcAccounts, rpcAccount)
}
return &clmrpc.ListAccountsResponse{
Accounts: rpcAccounts,
}, nil
}
func marshallAccount(a *account.Account) (*clmrpc.Account, error) {
var rpcState clmrpc.AccountState
switch a.State {
case account.StateInitiated, account.StatePendingOpen:
rpcState = clmrpc.AccountState_PENDING_OPEN
case account.StateOpen:
rpcState = clmrpc.AccountState_OPEN
case account.StateExpired:
rpcState = clmrpc.AccountState_EXPIRED
case account.StatePendingClosed:
rpcState = clmrpc.AccountState_PENDING_CLOSED
case account.StateClosed:
rpcState = clmrpc.AccountState_CLOSED
default:
return nil, fmt.Errorf("unknown state %v", a.State)
}
var closeTxHash chainhash.Hash
if a.CloseTx != nil {
closeTxHash = a.CloseTx.TxHash()
}
return &clmrpc.Account{
TraderKey: a.TraderKey.PubKey.SerializeCompressed(),
Outpoint: &clmrpc.OutPoint{
Txid: a.OutPoint.Hash[:],
OutputIndex: a.OutPoint.Index,
},
Value: uint32(a.Value),
ExpirationHeight: a.Expiry,
State: rpcState,
CloseTxid: closeTxHash[:],
}, nil
}
func (s *rpcServer) CloseAccount(ctx context.Context,
req *clmrpc.CloseAccountRequest) (*clmrpc.CloseAccountResponse, error) {
traderKey, err := btcec.ParsePubKey(req.TraderKey, btcec.S256())
if err != nil {
return nil, err
}
var closeOutputs []*wire.TxOut
if len(req.Outputs) > 0 {
closeOutputs = make([]*wire.TxOut, 0, len(req.Outputs))
for _, output := range req.Outputs {
// Make sure they've provided a valid output script.
_, err := txscript.ParsePkScript(output.Script)
if err != nil {
return nil, err
}
closeOutputs = append(closeOutputs, &wire.TxOut{
Value: int64(output.Value),
PkScript: output.Script,
})
}
}
closeTx, err := s.accountManager.CloseAccount(
ctx, traderKey, closeOutputs, atomic.LoadUint32(&s.bestHeight),
)
if err != nil {
return nil, err
}
closeTxHash := closeTx.TxHash()
return &clmrpc.CloseAccountResponse{
CloseTxid: closeTxHash[:],
}, nil
}
func (s *rpcServer) ModifyAccount(ctx context.Context,
req *clmrpc.ModifyAccountRequest) (
*clmrpc.ModifyAccountResponse, error) {
return nil, fmt.Errorf("unimplemented")
}
// SubmitOrder assembles all the information that is required to submit an order
// from the trader's lnd node, signs it and then sends the order to the server
// to be included in the auctioneer's order book.
func (s *rpcServer) SubmitOrder(ctx context.Context,
req *clmrpc.SubmitOrderRequest) (*clmrpc.SubmitOrderResponse, error) {
var o order.Order
switch requestOrder := req.Details.(type) {
case *clmrpc.SubmitOrderRequest_Ask:
a := requestOrder.Ask
kit, err := order.ParseRPCOrder(a.Version, a.Details)
if err != nil {
return nil, err
}
o = &order.Ask{
Kit: *kit,
MaxDuration: uint32(a.MaxDurationBlocks),
}
case *clmrpc.SubmitOrderRequest_Bid:
b := requestOrder.Bid
kit, err := order.ParseRPCOrder(b.Version, b.Details)
if err != nil {
return nil, err
}
o = &order.Bid{
Kit: *kit,
MinDuration: uint32(b.MinDurationBlocks),
}
default:
return nil, fmt.Errorf("invalid order request")
}
// Verify that the account exists.
acct, err := s.db.Account(o.Details().AcctKey)
if err != nil {
return nil, fmt.Errorf("cannot accept order: %v", err)
}
// Collect all the order data and sign it before sending it to the
// auction server.
serverParams, err := s.orderManager.PrepareOrder(ctx, o, acct)
if err != nil {
return nil, err
}
// Send the order to the server. If this fails, then the order is
// certain to never get into the order book. We don't need to keep it
// around in that case.
err = s.auctioneer.SubmitOrder(ctx, o, serverParams)
if err != nil {
// TODO(guggero): Put in state failed instead of removing?
if err2 := s.db.DelOrder(o.Nonce()); err2 != nil {
log.Errorf("Could not delete failed order: %v", err2)
}
// If there was something wrong with the information the user
// provided, then return this as a nice string instead of an
// error type.
if userErr, ok := err.(*order.UserError); ok {
return &clmrpc.SubmitOrderResponse{
Details: &clmrpc.SubmitOrderResponse_InvalidOrder{
InvalidOrder: userErr.Details,
},
}, nil
}
// Any other error we return normally as a gRPC status level
// error.
return nil, fmt.Errorf("error submitting order to auctioneer: "+
"%v", err)
}
// ServerOrder is accepted.
return &clmrpc.SubmitOrderResponse{
Details: &clmrpc.SubmitOrderResponse_Accepted{
Accepted: true,
},
}, nil
}
// ListOrders returns a list of all orders that is currently known to the trader
// client's local store. The state of each order is queried on the auction
// server and returned as well.
func (s *rpcServer) ListOrders(ctx context.Context, _ *clmrpc.ListOrdersRequest) (
*clmrpc.ListOrdersResponse, error) {
// Get all orders from our local store first.
dbOrders, err := s.db.GetOrders()
if err != nil {
return nil, err
}
// The RPC is split by order type so we have to separate them now.
asks := make([]*clmrpc.Ask, 0, len(dbOrders))
bids := make([]*clmrpc.Bid, 0, len(dbOrders))
for _, dbOrder := range dbOrders {
nonce := dbOrder.Nonce()
// Ask the server about the order's current status.
state, unitsUnfullfilled, err := s.auctioneer.OrderState(
ctx, nonce,
)
if err != nil {
return nil, fmt.Errorf("unable to query order state on"+
"server for order %v: %v", nonce.String(), err)
}
dbDetails := dbOrder.Details()
details := &clmrpc.Order{
UserSubKey: dbDetails.AcctKey.SerializeCompressed(),
RateFixed: int64(dbDetails.FixedRate),
Amt: int64(dbDetails.Amt),
FundingFeeRate: int64(dbDetails.FixedRate),
OrderNonce: nonce[:],
State: state.String(),
Units: uint32(dbDetails.Units),
UnitsUnfulfilled: unitsUnfullfilled,
}
switch o := dbOrder.(type) {
case *order.Ask:
rpcAsk := &clmrpc.Ask{
Details: details,
MaxDurationBlocks: int64(o.MaxDuration),
Version: uint32(o.Version),
}
asks = append(asks, rpcAsk)
case *order.Bid:
rpcBid := &clmrpc.Bid{
Details: details,
MinDurationBlocks: int64(o.MinDuration),
Version: uint32(o.Version),
}
bids = append(bids, rpcBid)
default:
return nil, fmt.Errorf("unknown order type: %v",
o)
}
}
return &clmrpc.ListOrdersResponse{
Asks: asks,
Bids: bids,
}, nil
}
// CancelOrder cancels the order on the server and updates the state of the
// local order accordingly.
func (s *rpcServer) CancelOrder(ctx context.Context,
req *clmrpc.CancelOrderRequest) (*clmrpc.CancelOrderResponse, error) {
var nonce order.Nonce
copy(nonce[:], req.OrderNonce)
err := s.auctioneer.CancelOrder(ctx, nonce)
if err != nil {
return nil, err
}
return &clmrpc.CancelOrderResponse{}, nil
}
// sendRejectBatch sends a reject message to the server with the properly
// decoded reason code and the full reason message as a string.
func (s *rpcServer) sendRejectBatch(batch *order.Batch, failure error) error {
msg := &clmrpc.ClientAuctionMessage_Reject{
Reject: &clmrpc.OrderMatchReject{
BatchId: batch.ID[:],
Reason: failure.Error(),
},
}
// Attach the status code to the message to give a bit more context.
switch {
case errors.Is(failure, order.ErrVersionMismatch):
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_BATCH_VERSION_MISMATCH
case errors.Is(failure, order.ErrMismatchErr):
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_SERVER_MISBEHAVIOR
default:
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_UNKNOWN
}
log.Infof("Sending batch rejection message for batch %x with "+
"code %v and message: %v", batch.ID, msg.Reject.ReasonCode,
failure)
// Send the message to the server. If a new error happens we return that
// one because we know the causing error has at least been logged at
// some point before.
err := s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
Msg: msg,
})
if err != nil {
return fmt.Errorf("error sending reject message: %v", err)
}
return failure
}
// sendAcceptBatch sends an accept message to the server with the list of order
// nonces that we accept in the batch.
func (s *rpcServer) sendAcceptBatch(batch *order.Batch) error {
// Prepare the list of nonces we accept by serializing them to a slice
// of byte slices.
nonces := make([][]byte, 0, len(batch.MatchedOrders))
idx := 0
for nonce := range batch.MatchedOrders {
nonces[idx] = nonce[:]
idx++
}
// Send the message to the server.
return s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
Msg: &clmrpc.ClientAuctionMessage_Accept{
Accept: &clmrpc.OrderMatchAccept{
BatchId: batch.ID[:],
OrderNonce: nonces,
},
},
})
}
// sendSignBatch sends a sign message to the server with the witness stacks of
// all accounts that are involved in the batch.
func (s *rpcServer) sendSignBatch(batch *order.Batch,
sigs order.BatchSignature) error {
// Prepare the list of witness stack messages and send them to the
// server.
rpcSigs := make(map[string]*clmrpc.AccountWitness)
for acctKey, witness := range sigs {
key := hex.EncodeToString(acctKey[:])
rpcSigs[key] = &clmrpc.AccountWitness{
Witness: witness,
}
}
return s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
Msg: &clmrpc.ClientAuctionMessage_Sign{
Sign: &clmrpc.OrderMatchSign{
BatchId: batch.ID[:],
AccountWitness: rpcSigs,
},
},
})
}