pool/rpcserver.go
2020-10-30 16:28:02 -07:00

2140 lines
62 KiB
Go

package pool
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"net"
"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/lndclient"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/auctioneer"
"github.com/lightninglabs/pool/chaninfo"
"github.com/lightninglabs/pool/clientdb"
"github.com/lightninglabs/pool/event"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/poolrpc"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightninglabs/pool/terms"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/tor"
)
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
lndClient lnrpc.LightningClient
auctioneer *auctioneer.Client
accountManager *account.Manager
orderManager *order.Manager
fundingManager *fundingMgr
// pendingOpenChannels is a channel through which we'll send received
// notifications for pending open channels resulting from a successful
// batch.
pendingOpenChannels chan<- *lnrpc.ChannelEventUpdate_PendingOpenChannel
quit chan struct{}
wg sync.WaitGroup
blockNtfnCancel func()
pendingOpenChannelStreamCancel func()
recoveryMutex sync.Mutex
recoveryPending bool
// wumboSupported is true if the backing lnd node supports wumbo
// channels.
wumboSupported bool
}
// accountStore is a clientdb.DB wrapper to implement the account.Store
// interface.
type accountStore struct {
*clientdb.DB
}
var _ account.Store = (*accountStore)(nil)
func (s *accountStore) PendingBatch() error {
_, _, err := s.DB.PendingBatch()
return err
}
// 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) *rpcServer {
accountStore := &accountStore{server.db}
lnd := &server.lndServices.LndServices
pendingOpenChannels := make(chan *lnrpc.ChannelEventUpdate_PendingOpenChannel)
quit := make(chan struct{})
return &rpcServer{
server: server,
lndServices: lnd,
lndClient: server.lndClient,
auctioneer: server.AuctioneerClient,
accountManager: account.NewManager(&account.ManagerConfig{
Store: accountStore,
Auctioneer: server.AuctioneerClient,
Wallet: lnd.WalletKit,
Signer: lnd.Signer,
ChainNotifier: lnd.ChainNotifier,
TxSource: lnd.Client,
TxFeeEstimator: lnd.Client,
TxLabelPrefix: server.cfg.TxLabelPrefix,
}),
orderManager: order.NewManager(&order.ManagerConfig{
Store: server.db,
AcctStore: accountStore,
Lightning: lnd.Client,
Wallet: lnd.WalletKit,
Signer: lnd.Signer,
}),
fundingManager: &fundingMgr{
db: server.db,
walletKit: lnd.WalletKit,
lightningClient: lnd.Client,
baseClient: server.lndClient,
pendingOpenChannels: pendingOpenChannels,
quit: quit,
batchStepTimeout: defaultBatchStepTimeout,
newNodesOnly: server.cfg.NewNodesOnly,
},
pendingOpenChannels: pendingOpenChannels,
quit: quit,
}
}
// 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
}
rpcLog.Infof("Starting trader server")
ctx := context.Background()
lndCtx, lndCancel := context.WithTimeout(ctx, getInfoTimeout)
defer lndCancel()
info, err := s.lndClient.GetInfo(lndCtx, &lnrpc.GetInfoRequest{})
if err != nil {
return fmt.Errorf("error in GetInfo: %v", err)
}
_, s.wumboSupported = info.Features[uint32(lnwire.WumboChannelsRequired)]
if !s.wumboSupported {
_, s.wumboSupported = info.Features[uint32(lnwire.WumboChannelsOptional)]
}
rpcLog.Infof("Connected to lnd node %v with pubkey %v", info.Alias,
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)
// Subscribe to pending open channel notifications. This will be useful
// when we're creating channels with a matched order as part of a batch.
streamCtx, streamCancel := context.WithCancel(ctx)
s.pendingOpenChannelStreamCancel = streamCancel
subStream, err := s.lndClient.SubscribeChannelEvents(
streamCtx, &lnrpc.ChannelEventSubscription{},
)
if err != nil {
return err
}
// 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(2)
go s.serverHandler(blockChan, blockErrChan)
go s.consumePendingOpenChannels(subStream)
rpcLog.Infof("Trader server is now active")
return nil
}
// consumePendingOpenChannels consumes pending open channel events from the
// stream and notifies them if the trader currently has an ongoing batch.
func (s *rpcServer) consumePendingOpenChannels(
subStream lnrpc.Lightning_SubscribeChannelEventsClient) {
defer s.wg.Done()
for {
select {
case <-s.quit:
return
default:
}
msg, err := subStream.Recv()
if err != nil {
select {
case <-s.quit:
return
default:
}
rpcLog.Errorf("Unable to read channel event: %v", err)
continue
}
// Skip any events other than the pending open channel one.
channel, ok := msg.Channel.(*lnrpc.ChannelEventUpdate_PendingOpenChannel)
if !ok {
continue
}
// If we don't have a pending batch, then there's no need to
// notify any pending open channels..
if !s.orderManager.HasPendingBatch() {
continue
}
select {
case s.pendingOpenChannels <- channel:
case <-s.quit:
return
}
}
}
// Stop stops the server.
func (s *rpcServer) Stop() error {
if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) {
return nil
}
rpcLog.Info("Trader server stopping")
s.accountManager.Stop()
s.orderManager.Stop()
if err := s.auctioneer.Stop(); err != nil {
rpcLog.Errorf("Error closing server stream: %v")
}
close(s.quit)
// We call this before Wait to ensure the goroutine is stopped by this
// call.
s.pendingOpenChannelStreamCancel()
s.wg.Wait()
s.blockNtfnCancel()
rpcLog.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
}
rpcLog.Debugf("Received message from the server: %v", msg)
err := s.handleServerMessage(msg)
// Only shut down if this was a terminal error, and not
// a batch reject (should rarely happen, but it's
// possible).
if err != nil && !errors.Is(err, order.ErrMismatchErr) {
rpcLog.Errorf("Error handling server message: %v",
err)
signal.RequestShutdown()
}
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 {
rpcLog.Errorf("Error in server stream: %v", err)
err := s.auctioneer.HandleServerShutdown(err)
if err != nil {
rpcLog.Errorf("Error closing stream: %v",
err)
}
}
case height := <-blockChan:
rpcLog.Infof("Received new block notification: height=%v",
height)
s.updateHeight(height)
case err := <-blockErrChan:
if err != nil {
rpcLog.Errorf("Unable to receive block "+
"notification: %v", err)
signal.RequestShutdown()
}
// 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 *poolrpc.ServerAuctionMessage) error {
switch msg := rpcMsg.Msg.(type) {
// A new batch has been assembled with some of our orders.
case *poolrpc.ServerAuctionMessage_Prepare:
// Parse and formally validate what we got from the server.
rpcLog.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 {
// If we aren't able to parse the batch for some
// reason, then we'll send a reject message.
log.Error("unable to parse batch: %v", err)
return s.sendRejectBatch(batch, err)
}
rpcLog.Infof("Received PrepareMsg for batch=%x, num_orders=%v",
batch.ID[:], len(batch.MatchedOrders))
// Let's store an event for each order in the batch that we did
// receive a prepare message.
if err := s.server.db.StoreBatchEvents(
batch, order.MatchStatePrepare,
poolrpc.MatchRejectReason_NONE,
); err != nil {
rpcLog.Errorf("Unable to store order events: %v", err)
}
// The prepare message can be sent over and over again if the
// batch needs adjustment. Clear all previous shims.
if s.orderManager.HasPendingBatch() {
pendingBatch := s.orderManager.PendingBatch()
orderFetcher := s.server.db.GetOrder
err := pendingBatch.CancelPendingFundingShims(
s.lndClient, orderFetcher,
)
if err != nil {
// CancelPendingFundingShims only returns hard
// errors that justify us rejecting the batch.
rpcLog.Errorf("Error clearing previous batch: "+
"%v", err)
return s.sendRejectBatch(batch, err)
}
// Also abandon any channels that might still be pending
// from a previous round of the same batch or a previous
// batch that we didn't make it into the final round.
err = pendingBatch.AbandonCanceledChannels(
s.lndServices.WalletKit, s.lndClient,
orderFetcher,
)
if err != nil {
// AbandonCanceledChannels only returns hard
// errors that justify us rejecting the batch.
rpcLog.Errorf("Error abandoning channels from "+
"last batch: %v", err)
return s.sendRejectBatch(batch, 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.
rpcLog.Errorf("Error validating batch: %v", err)
return s.sendRejectBatch(batch, err)
}
// As we need to change our behavior if the node has any Tor
// addresses, we'll fetch the current state of our advertised
// addrs now.
nodeInfo, err := s.lndServices.Client.GetInfo(
context.Background(),
)
if err != nil {
log.Errorf("error in GetInfo: %v", err)
return s.sendRejectBatch(
batch, fmt.Errorf("internal error"),
)
}
// Before we accept the batch, we'll finish preparations on our
// end which include applying any order match predicates,
// connecting out to peers, and registering funding shim.
err = s.fundingManager.prepChannelFunding(
batch, nodeHasTorAddrs(nodeInfo.Uris),
)
if err != nil {
rpcLog.Warnf("Error preparing channel funding: %v",
err)
return s.sendRejectBatch(batch, err)
}
// Accept the match now.
err = s.sendAcceptBatch(batch)
if err != nil {
rpcLog.Errorf("Error sending accept msg: %v", err)
return s.sendRejectBatch(batch, err)
}
case *poolrpc.ServerAuctionMessage_Sign:
// 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.
batch := s.orderManager.PendingBatch()
channelKeys, err := s.fundingManager.batchChannelSetup(batch)
if err != nil {
rpcLog.Errorf("Error setting up channels: %v", err)
return s.sendRejectBatch(batch, err)
}
rpcLog.Infof("Received OrderMatchSignBegin for batch=%x, "+
"num_orders=%v", batch.ID[:], len(batch.MatchedOrders))
// Sign for the accounts in the batch.
bestHeight := atomic.LoadUint32(&s.bestHeight)
sigs, err := s.orderManager.BatchSign(bestHeight)
if err != nil {
rpcLog.Errorf("Error signing batch: %v", err)
return s.sendRejectBatch(batch, err)
}
err = s.sendSignBatch(batch, sigs, channelKeys)
if err != nil {
rpcLog.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 *poolrpc.ServerAuctionMessage_Finalize:
rpcLog.Tracef("Received finalize msg from server, batch_id=%x: %v",
msg.Finalize.BatchId, spew.Sdump(msg))
rpcLog.Infof("Received FinalizeMsg for batch=%x",
msg.Finalize.BatchId)
// Before finalizing the batch, we want to know what accounts
// were involved so we can start watching them again. Query the
// pending batch now as BatchFinalize below will set it to nil.
batch := s.orderManager.PendingBatch()
var batchID order.BatchID
copy(batchID[:], msg.Finalize.BatchId)
err := s.orderManager.BatchFinalize(batchID)
if err != nil {
return fmt.Errorf("error finalizing batch: %v", err)
}
// We've successfully processed the finalize message, let's
// store an event for this for all orders that were involved on
// our side.
if err := s.server.db.StoreBatchEvents(
batch, order.MatchStateFinalized,
poolrpc.MatchRejectReason_NONE,
); err != nil {
rpcLog.Errorf("Unable to store order events: %v", err)
}
// Accounts that were updated in the batch need to start new
// confirmation watchers, now that we expect a batch TX to be
// published.
matchedAccounts := make(
[]*btcec.PublicKey, len(batch.AccountDiffs),
)
for idx, acct := range batch.AccountDiffs {
matchedAccounts[idx] = acct.AccountKey
}
return s.accountManager.WatchMatchedAccounts(
context.Background(), matchedAccounts,
)
default:
return fmt.Errorf("unknown server message: %v", msg)
}
return nil
}
func (s *rpcServer) QuoteAccount(ctx context.Context,
req *poolrpc.QuoteAccountRequest) (*poolrpc.QuoteAccountResponse, error) {
// Determine the desired transaction fee.
confTarget := req.GetConfTarget()
if confTarget < 1 {
return nil, fmt.Errorf("confirmation target must be " +
"greater than 0")
}
feeRate, totalFee, err := s.accountManager.QuoteAccount(
ctx, btcutil.Amount(req.AccountValue), confTarget,
)
if err != nil {
return nil, err
}
return &poolrpc.QuoteAccountResponse{
MinerFeeRateSatPerKw: uint64(feeRate),
MinerFeeTotal: uint64(totalFee),
}, nil
}
func (s *rpcServer) InitAccount(ctx context.Context,
req *poolrpc.InitAccountRequest) (*poolrpc.Account, error) {
bestHeight := atomic.LoadUint32(&s.bestHeight)
// Determine the desired expiration value, can be relative or absolute.
var expiryHeight uint32
switch {
case req.GetAbsoluteHeight() != 0:
expiryHeight = req.GetAbsoluteHeight()
case req.GetRelativeHeight() != 0:
expiryHeight = req.GetRelativeHeight() + bestHeight
default:
return nil, fmt.Errorf("either relative or absolute height " +
"must be specified")
}
// Determine the desired transaction fee.
confTarget := req.GetConfTarget()
if confTarget < 1 {
return nil, fmt.Errorf("confirmation target must be " +
"greater than 0")
}
account, err := s.accountManager.InitAccount(
ctx, btcutil.Amount(req.AccountValue), expiryHeight,
bestHeight, confTarget,
)
if err != nil {
return nil, err
}
return marshallAccount(account)
}
func (s *rpcServer) ListAccounts(ctx context.Context,
req *poolrpc.ListAccountsRequest) (*poolrpc.ListAccountsResponse, error) {
accounts, err := s.server.db.Accounts()
if err != nil {
return nil, err
}
rpcAccounts := make([]*poolrpc.Account, 0, len(accounts))
for _, account := range accounts {
rpcAccount, err := marshallAccount(account)
if err != nil {
return nil, err
}
rpcAccounts = append(rpcAccounts, rpcAccount)
}
// For each account, we'll need to compute the available balance, which
// requires us to sum up all the debits from outstanding orders.
orders, err := s.server.db.GetOrders()
if err != nil {
return nil, err
}
// Get the current fee schedule so we can compute the worst-case
// account debit assuming all our standing orders were matched.
terms, err := s.auctioneer.Terms(ctx)
if err != nil {
return nil, fmt.Errorf("unable to query auctioneer terms: %v",
err)
}
// For each active account, consume the worst-case account delta if the
// order were to be matched.
accountDebits := make(map[[33]byte]btcutil.Amount)
auctionFeeSchedule := terms.FeeSchedule()
for _, account := range accounts {
var (
debitAmt btcutil.Amount
acctKey [33]byte
)
copy(
acctKey[:],
account.TraderKey.PubKey.SerializeCompressed(),
)
// We'll make sure to assumulate a distinct sum for each
// outstanding account the user has.
for _, order := range orders {
if order.Details().AcctKey != acctKey {
continue
}
debitAmt += order.ReservedValue(auctionFeeSchedule)
}
accountDebits[acctKey] = debitAmt
}
// Finally, we'll populate the available balance value for each of the
// existing accounts.
for _, rpcAccount := range rpcAccounts {
var acctKey [33]byte
copy(acctKey[:], rpcAccount.TraderKey)
accountDebit := accountDebits[acctKey]
availableBalance := rpcAccount.Value - uint64(accountDebit)
rpcAccount.AvailableBalance = availableBalance
}
return &poolrpc.ListAccountsResponse{
Accounts: rpcAccounts,
}, nil
}
func marshallAccount(a *account.Account) (*poolrpc.Account, error) {
var rpcState poolrpc.AccountState
switch a.State {
case account.StateInitiated, account.StatePendingOpen:
rpcState = poolrpc.AccountState_PENDING_OPEN
case account.StatePendingUpdate:
rpcState = poolrpc.AccountState_PENDING_UPDATE
case account.StateOpen:
rpcState = poolrpc.AccountState_OPEN
case account.StateExpired:
rpcState = poolrpc.AccountState_EXPIRED
case account.StatePendingClosed:
rpcState = poolrpc.AccountState_PENDING_CLOSED
case account.StateClosed:
rpcState = poolrpc.AccountState_CLOSED
case account.StateCanceledAfterRecovery:
rpcState = poolrpc.AccountState_RECOVERY_FAILED
case account.StatePendingBatch:
rpcState = poolrpc.AccountState_PENDING_BATCH
default:
return nil, fmt.Errorf("unknown state %v", a.State)
}
// The latest transaction is only known after the account has been
// funded.
var latestTxHash chainhash.Hash
if a.LatestTx != nil {
latestTxHash = a.LatestTx.TxHash()
}
return &poolrpc.Account{
TraderKey: a.TraderKey.PubKey.SerializeCompressed(),
Outpoint: &poolrpc.OutPoint{
Txid: a.OutPoint.Hash[:],
OutputIndex: a.OutPoint.Index,
},
Value: uint64(a.Value),
ExpirationHeight: a.Expiry,
State: rpcState,
LatestTxid: latestTxHash[:],
}, nil
}
// DepositAccount handles a trader's request to deposit funds into the specified
// account by spending the specified inputs.
func (s *rpcServer) DepositAccount(ctx context.Context,
req *poolrpc.DepositAccountRequest) (*poolrpc.DepositAccountResponse, error) {
rpcLog.Infof("Depositing %v into acct=%x",
btcutil.Amount(req.AmountSat), req.TraderKey)
// Ensure the trader key is well formed.
traderKey, err := btcec.ParsePubKey(req.TraderKey, btcec.S256())
if err != nil {
return nil, err
}
// Enforce a minimum fee rate of 253 sat/kw.
feeRate := chainfee.SatPerKWeight(req.FeeRateSatPerKw)
if feeRate < chainfee.FeePerKwFloor {
return nil, fmt.Errorf("fee rate of %d sat/kw is too low, "+
"minimum is %d sat/kw", feeRate, chainfee.FeePerKwFloor)
}
// Proceed to process the deposit and map its response to the RPC's
// response.
modifiedAccount, tx, err := s.accountManager.DepositAccount(
ctx, traderKey, btcutil.Amount(req.AmountSat), feeRate,
atomic.LoadUint32(&s.bestHeight),
)
if err != nil {
return nil, err
}
rpcModifiedAccount, err := marshallAccount(modifiedAccount)
if err != nil {
return nil, err
}
txHash := tx.TxHash()
return &poolrpc.DepositAccountResponse{
Account: rpcModifiedAccount,
DepositTxid: txHash[:],
}, nil
}
// WithdrawAccount handles a trader's request to withdraw funds from the
// specified account by spending the current account output to the specified
// outputs.
func (s *rpcServer) WithdrawAccount(ctx context.Context,
req *poolrpc.WithdrawAccountRequest) (*poolrpc.WithdrawAccountResponse, error) {
rpcLog.Infof("Withdrawing from acct=%x", req.TraderKey)
// Ensure the trader key is well formed.
traderKey, err := btcec.ParsePubKey(req.TraderKey, btcec.S256())
if err != nil {
return nil, err
}
// Ensure the outputs we'll withdraw to are well formed.
if len(req.Outputs) == 0 {
return nil, errors.New("missing outputs for withdrawal")
}
outputs, err := s.parseRPCOutputs(req.Outputs)
if err != nil {
return nil, err
}
// Enforce a minimum fee rate of 253 sat/kw.
feeRate := chainfee.SatPerKWeight(req.FeeRateSatPerKw)
if feeRate < chainfee.FeePerKwFloor {
return nil, fmt.Errorf("fee rate of %d sat/kw is too low, "+
"minimum is %d sat/kw", feeRate, chainfee.FeePerKwFloor)
}
// Proceed to process the withdrawal and map its response to the RPC's
// response.
modifiedAccount, tx, err := s.accountManager.WithdrawAccount(
ctx, traderKey, outputs, feeRate,
atomic.LoadUint32(&s.bestHeight),
)
if err != nil {
return nil, err
}
rpcModifiedAccount, err := marshallAccount(modifiedAccount)
if err != nil {
return nil, err
}
txHash := tx.TxHash()
return &poolrpc.WithdrawAccountResponse{
Account: rpcModifiedAccount,
WithdrawTxid: txHash[:],
}, nil
}
// BumpAccountFee attempts to bump the fee of an account's transaction through
// child-pays-for-parent (CPFP). Since the CPFP is performed through the backing
// lnd node, the account transaction must contain an output under its control
// for a successful bump. If a CPFP has already been performed for an account,
// and this RPC is invoked again, then a replacing transaction (RBF) of the
// child will be broadcast.
func (s *rpcServer) BumpAccountFee(ctx context.Context,
req *poolrpc.BumpAccountFeeRequest) (*poolrpc.BumpAccountFeeResponse, error) {
traderKey, err := btcec.ParsePubKey(req.TraderKey, btcec.S256())
if err != nil {
return nil, err
}
feeRate := chainfee.SatPerKWeight(req.FeeRateSatPerKw)
err = s.accountManager.BumpAccountFee(ctx, traderKey, feeRate)
if err != nil {
return nil, err
}
return &poolrpc.BumpAccountFeeResponse{}, nil
}
// CloseAccount handles a trader's request to close the specified account.
func (s *rpcServer) CloseAccount(ctx context.Context,
req *poolrpc.CloseAccountRequest) (*poolrpc.CloseAccountResponse, error) {
rpcLog.Infof("Closing acct=%x", req.TraderKey)
traderKey, err := btcec.ParsePubKey(req.TraderKey, btcec.S256())
if err != nil {
return nil, err
}
// Ensure a valid fee expression was provided.
var feeExpr account.FeeExpr
switch dest := req.FundsDestination.(type) {
// The fee is expressed as a combination of an output along with a fee
// rate.
case *poolrpc.CloseAccountRequest_OutputWithFee:
// Parse the output script if one was provided and ensure it's
// valid.
var pkScript []byte
if dest.OutputWithFee.Address != "" {
var err error
pkScript, err = s.parseOutputScript(
dest.OutputWithFee.Address,
)
if err != nil {
return nil, err
}
}
// Parse the provided fee rate.
var feeRate chainfee.SatPerKWeight
switch feeExpr := dest.OutputWithFee.Fees.(type) {
case *poolrpc.OutputWithFee_ConfTarget:
var err error
feeRate, err = s.lndServices.WalletKit.EstimateFee(
ctx, int32(feeExpr.ConfTarget),
)
if err != nil {
return nil, err
}
case *poolrpc.OutputWithFee_FeeRateSatPerKw:
feeRate = chainfee.SatPerKWeight(feeExpr.FeeRateSatPerKw)
}
// Enforce a minimum fee rate of 253 sat/kw.
if feeRate < chainfee.FeePerKwFloor {
return nil, fmt.Errorf("fee rate of %v is too low, "+
"minimum is %v", feeRate, chainfee.FeePerKwFloor)
}
feeExpr = &account.OutputWithFee{
PkScript: pkScript,
FeeRate: feeRate,
}
// The fee is expressed as implicitly defined by the total output value
// of the provided outputs.
case *poolrpc.CloseAccountRequest_Outputs:
if len(dest.Outputs.Outputs) == 0 {
return nil, errors.New("no outputs provided")
}
outputs, err := s.parseRPCOutputs(dest.Outputs.Outputs)
if err != nil {
return nil, err
}
feeExpr = account.OutputsWithImplicitFee(outputs)
case nil:
return nil, errors.New("a funds destination must be specified")
}
dbOrders, err := s.server.db.GetOrders()
if err != nil {
return nil, err
}
var openNonces []order.Nonce
for _, dbOrder := range dbOrders {
orderDetails := dbOrder.Details()
nonce := orderDetails.Nonce()
// To ensure we have the latest order state, we'll consult with
// the auctioneer's state.
orderStateResp, err := s.auctioneer.OrderState(ctx, nonce)
if err != nil {
return nil, err
}
orderState, err := rpcOrderStateToDBState(orderStateResp.State)
if err != nil {
return nil, err
}
// If the order isn't in the base state, then we'll skip it as
// it isn't considered an "active" order.
switch orderState {
case order.StateFailed, order.StateExpired,
order.StateCanceled, order.StateExecuted:
continue
}
if bytes.Equal(orderDetails.AcctKey[:], req.TraderKey) {
openNonces = append(openNonces, orderDetails.Nonce())
}
}
// We don't allow an account to be closed if it has open orders so they
// don't dangle in the order book on the server's side.
if len(openNonces) > 0 {
return nil, fmt.Errorf("acct=%x has open orders, cancel them "+
"before closing: %v", req.TraderKey, openNonces)
}
// Proceed to close the requested account with the parsed fee
// expression.
closeTx, err := s.accountManager.CloseAccount(
ctx, traderKey, feeExpr, atomic.LoadUint32(&s.bestHeight),
)
if err != nil {
return nil, err
}
closeTxHash := closeTx.TxHash()
return &poolrpc.CloseAccountResponse{
CloseTxid: closeTxHash[:],
}, nil
}
// parseOutputScript parses the output script from the string representation of
// an on-chain address and ensures it's valid for the current network.
func (s *rpcServer) parseOutputScript(addrStr string) ([]byte, error) {
addr, err := btcutil.DecodeAddress(addrStr, s.lndServices.ChainParams)
if err != nil {
return nil, err
}
if !addr.IsForNet(s.lndServices.ChainParams) {
return nil, fmt.Errorf("invalid address %v for %v "+
"network", addr.String(),
s.lndServices.ChainParams.Name)
}
return txscript.PayToAddrScript(addr)
}
// parseRPCOutputs maps []*poolrpc.Output -> []*wire.TxOut.
func (s *rpcServer) parseRPCOutputs(outputs []*poolrpc.Output) ([]*wire.TxOut,
error) {
res := make([]*wire.TxOut, 0, len(outputs))
for _, output := range outputs {
outputScript, err := s.parseOutputScript(output.Address)
if err != nil {
return nil, err
}
res = append(res, &wire.TxOut{
Value: int64(output.ValueSat),
PkScript: outputScript,
})
}
return res, nil
}
func (s *rpcServer) RecoverAccounts(ctx context.Context,
_ *poolrpc.RecoverAccountsRequest) (*poolrpc.RecoverAccountsResponse,
error) {
log.Infof("Attempting to recover accounts...")
s.recoveryMutex.Lock()
if s.recoveryPending {
defer s.recoveryMutex.Unlock()
return nil, fmt.Errorf("recovery already in progress")
}
s.recoveryPending = true
s.recoveryMutex.Unlock()
// Prepare the keys we are going to try. Possibly not all of them will
// be used.
acctKeys, err := account.GenerateRecoveryKeys(
ctx, s.lndServices.WalletKit,
)
if err != nil {
return nil, fmt.Errorf("error generating keys: %v", err)
}
// The auctioneer client will try to recover accounts for these keys as
// long as the auctioneer is able to find them in its database. If a
// certain number of keys result in an "account not found" error, the
// client will stop trying.
recoveredAccounts, err := s.auctioneer.RecoverAccounts(ctx, acctKeys)
if err != nil {
return nil, fmt.Errorf("error performing recovery: %v", err)
}
// Store the recovered accounts now and start watching them. If anything
// went wrong during recovery before, no account is stored yet. This is
// nice since it allows us to try recovery multiple times until it
// actually works.
numRecovered := len(recoveredAccounts)
for _, acct := range recoveredAccounts {
err = s.accountManager.RecoverAccount(ctx, acct)
if err != nil {
// If something goes wrong for one account we still want
// to continue with the others.
numRecovered--
rpcLog.Errorf("error storing recovered account: %v", err)
}
}
s.recoveryMutex.Lock()
s.recoveryPending = false
s.recoveryMutex.Unlock()
return &poolrpc.RecoverAccountsResponse{
NumRecoveredAccounts: uint32(numRecovered),
}, nil
}
// 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 *poolrpc.SubmitOrderRequest) (*poolrpc.SubmitOrderResponse, error) {
var o order.Order
switch requestOrder := req.Details.(type) {
case *poolrpc.SubmitOrderRequest_Ask:
a := requestOrder.Ask
kit, err := order.ParseRPCOrder(
a.Version, a.LeaseDurationBlocks, a.Details,
)
if err != nil {
return nil, err
}
o = &order.Ask{
Kit: *kit,
}
case *poolrpc.SubmitOrderRequest_Bid:
b := requestOrder.Bid
kit, err := order.ParseRPCOrder(
b.Version, b.LeaseDurationBlocks, b.Details,
)
if err != nil {
return nil, err
}
nodeTier, err := unmarshallNodeTier(b.MinNodeTier)
if err != nil {
return nil, err
}
o = &order.Bid{
Kit: *kit,
MinNodeTier: nodeTier,
}
default:
return nil, fmt.Errorf("invalid order request")
}
// Now that we now how large the order is, ensure that if it's a
// wumbo-sized order, then the backing lnd node is advertising wumbo
// support.
if o.Details().Amt > lnd.MaxBtcFundingAmount && !s.wumboSupported {
return nil, fmt.Errorf("order of %v is wumbo sized, but "+
"lnd node isn't signalling wumbo", o.Details().Amt)
}
// Verify that the account exists.
acctKey, err := btcec.ParsePubKey(
o.Details().AcctKey[:], btcec.S256(),
)
if err != nil {
return nil, err
}
acct, err := s.server.db.Account(acctKey)
if err != nil {
return nil, fmt.Errorf("cannot accept order: %v", err)
}
// We'll only allow orders for accounts that present in an open state,
// or have a pending update or batch. On the server-side if we have a
// pending update we won't be matched, but this lets us place our orders
// early so we can join the earliest available batch.
switch acct.State {
case account.StateOpen, account.StatePendingUpdate,
account.StatePendingBatch:
break
default:
return nil, fmt.Errorf("acct=%x is in state %v, cannot "+
"make order", o.Details().AcctKey[:], acct.State)
}
// We also need to know the current maximum order duration.
terms, err := s.auctioneer.Terms(ctx)
if err != nil {
return nil, fmt.Errorf("could not query auctioneer terms: %v",
err)
}
// If the market isn't currently accepting orders for this particular
// lease duration, then we'll exit here as the order will be rejected.
leaseDuration := o.Details().LeaseDuration
if _, ok := terms.LeaseDurations[leaseDuration]; !ok {
return nil, fmt.Errorf("invalid channel lease duration %v "+
"blocks, active durations are: %v",
leaseDuration, terms.LeaseDurations)
}
// Collect all the order data and sign it before sending it to the
// auction server.
serverParams, err := s.orderManager.PrepareOrder(ctx, o, acct, terms)
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 {
// The server rejected the order. We keep it around for now,
// failed orders can be filtered by specifying --active_only
// when listing orders.
err2 := s.server.db.UpdateOrder(
o.Nonce(), order.StateModifier(order.StateFailed),
)
if err2 != nil {
rpcLog.Errorf("Could not update 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 {
rpcLog.Warnf("Invalid order details: %v", userErr)
return &poolrpc.SubmitOrderResponse{
Details: &poolrpc.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)
}
rpcLog.Infof("New order submitted: nonce=%v, type=%v", o.Nonce(), o.Type())
// ServerOrder is accepted.
orderNonce := o.Nonce()
return &poolrpc.SubmitOrderResponse{
Details: &poolrpc.SubmitOrderResponse_AcceptedOrderNonce{
AcceptedOrderNonce: orderNonce[:],
},
}, 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,
req *poolrpc.ListOrdersRequest) (*poolrpc.ListOrdersResponse, error) {
// Because we made sure every order has a creation timestamp in the
// events bucket, we can now access the orders in a sorted way if we
// first grab all creation events. This might be a bit less efficient
// because we first need to query the events bucket. But this can easily
// be solved by adding a write-through cache for the events, then that
// bucket serves both as an unique order sequence as well as the event
// source and is really fast to access.
creationEvents, err := s.server.db.AllEvents(event.TypeOrderCreated)
if err != nil {
return nil, fmt.Errorf("error querying order creation events: "+
"%v", err)
}
// If we cannot query the auctioneer, we just use an empty fee
// schedule, as it is only used for calculating the reserved value.
var feeSchedule terms.FeeSchedule = terms.NewLinearFeeSchedule(0, 0)
auctioneerTerms, err := s.auctioneer.Terms(ctx)
if err != nil {
log.Warnf("unable to query auctioneer terms: %v", err)
} else {
feeSchedule = auctioneerTerms.FeeSchedule()
}
// The RPC is split by order type so we have to separate them now.
asks := make([]*poolrpc.Ask, 0, len(creationEvents))
bids := make([]*poolrpc.Bid, 0, len(creationEvents))
for _, evt := range creationEvents {
orderCreateEvent, ok := evt.(*clientdb.CreatedEvent)
if !ok {
return nil, fmt.Errorf("invalid order create event: %v",
evt)
}
dbOrder, err := s.server.db.GetOrder(orderCreateEvent.Nonce())
if err != nil {
return nil, err
}
nonce := dbOrder.Nonce()
dbDetails := dbOrder.Details()
// Skip order if it is archived and only active orders are
// requested.
if dbDetails.State.Archived() && req.ActiveOnly {
continue
}
orderState, err := dbOrderStateToRPCState(dbDetails.State)
if err != nil {
return nil, err
}
var rpcEvents []*poolrpc.OrderEvent
if req.Verbose {
dbEvents, err := s.server.db.GetOrderEvents(nonce)
if err != nil {
return nil, err
}
rpcEvents, err = dbEventsToRPCEvents(dbEvents)
if err != nil {
return nil, err
}
}
details := &poolrpc.Order{
TraderKey: dbDetails.AcctKey[:],
RateFixed: dbDetails.FixedRate,
Amt: uint64(dbDetails.Amt),
MaxBatchFeeRateSatPerKw: uint64(
dbDetails.MaxBatchFeeRate,
),
OrderNonce: nonce[:],
State: orderState,
Units: uint32(dbDetails.Units),
UnitsUnfulfilled: uint32(dbDetails.UnitsUnfulfilled),
ReservedValueSat: uint64(
dbOrder.ReservedValue(feeSchedule),
),
CreationTimestampNs: uint64(evt.Timestamp().UnixNano()),
Events: rpcEvents,
MinUnitsMatch: uint32(dbOrder.Details().MinUnitsMatch),
}
switch o := dbOrder.(type) {
case *order.Ask:
rpcAsk := &poolrpc.Ask{
Details: details,
LeaseDurationBlocks: dbDetails.LeaseDuration,
Version: uint32(o.Version),
}
asks = append(asks, rpcAsk)
case *order.Bid:
nodeTier, err := auctioneer.MarshallNodeTier(o.MinNodeTier)
if err != nil {
return nil, err
}
rpcBid := &poolrpc.Bid{
Details: details,
LeaseDurationBlocks: dbDetails.LeaseDuration,
Version: uint32(o.Version),
MinNodeTier: nodeTier,
}
bids = append(bids, rpcBid)
default:
return nil, fmt.Errorf("unknown order type: %v",
o)
}
}
return &poolrpc.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 *poolrpc.CancelOrderRequest) (*poolrpc.CancelOrderResponse, error) {
var nonce order.Nonce
copy(nonce[:], req.OrderNonce)
o, err := s.server.db.GetOrder(nonce)
if err != nil {
return nil, err
}
switch o.Details().State {
// The order has already been canceled, just return now.
case order.StateCanceled:
return &poolrpc.CancelOrderResponse{}, nil
// The order has reached a terminal state so there's no point in
// canceling it.
case order.StateExecuted, order.StateExpired, order.StateFailed:
return nil, fmt.Errorf("unable to cancel order in terminal "+
"state %v", o.Details().State)
default:
break
}
rpcLog.Infof("Cancelling order_nonce=%v", nonce)
// We cancel an order in two phases. First, we'll cancel the order on
// the server-side.
err = s.auctioneer.CancelOrder(ctx, o.Details().Preimage)
if err != nil {
return nil, err
}
// Now that we've cancelled things on the server-side, we'll update our
// local state to reflect this change in the order. If we crash here,
// then we'll sync up the order state once we restart again.
err = s.server.db.UpdateOrder(
nonce, order.StateModifier(order.StateCanceled),
)
if err != nil {
return nil, err
}
return &poolrpc.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 {
// As we're rejecting this batch, we'll now cancel all funding shims
// that we may have registered since we may be matched with a distinct
// set of channels if this batch is repeated.
err := batch.CancelPendingFundingShims(
s.lndClient,
func(o order.Nonce) (order.Order, error) {
return s.server.db.GetOrder(o)
},
)
if err != nil {
return err
}
msg := &poolrpc.ClientAuctionMessage_Reject{
Reject: &poolrpc.OrderMatchReject{
BatchId: batch.ID[:],
Reason: failure.Error(),
},
}
// Attach the status code to the message to give a bit more context.
var partialReject *matchRejectErr
switch {
case errors.Is(failure, order.ErrVersionMismatch):
msg.Reject.ReasonCode = poolrpc.OrderMatchReject_BATCH_VERSION_MISMATCH
// Track this reject by adding an event to our orders.
err := s.server.db.StoreBatchEvents(
batch, order.MatchStateRejected,
poolrpc.MatchRejectReason_BATCH_VERSION_MISMATCH,
)
if err != nil {
rpcLog.Errorf("Could not store match event: %v", err)
}
case errors.Is(failure, order.ErrMismatchErr):
msg.Reject.ReasonCode = poolrpc.OrderMatchReject_SERVER_MISBEHAVIOR
// Track this reject by adding an event to our orders.
err := s.server.db.StoreBatchEvents(
batch, order.MatchStateRejected,
poolrpc.MatchRejectReason_SERVER_MISBEHAVIOR,
)
if err != nil {
rpcLog.Errorf("Could not store match event: %v", err)
}
case errors.As(failure, &partialReject):
msg.Reject.ReasonCode = poolrpc.OrderMatchReject_PARTIAL_REJECT
msg.Reject.RejectedOrders = make(map[string]*poolrpc.OrderReject)
for nonce, reject := range partialReject.rejectedOrders {
msg.Reject.RejectedOrders[nonce.String()] = reject
}
// Track this reject by adding an event to our orders with the
// specific reject code for each of rejected orders.
err := s.server.db.StoreBatchPartialRejectEvents(
batch, partialReject.rejectedOrders,
)
if err != nil {
rpcLog.Errorf("Could not store match event: %v", err)
}
default:
msg.Reject.ReasonCode = poolrpc.OrderMatchReject_UNKNOWN
}
rpcLog.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(&poolrpc.ClientAuctionMessage{
Msg: msg,
})
if err != nil {
return fmt.Errorf("error sending reject message: %v", err)
}
// We have handled the batch failure and informed the auctioneer. We
// have done our job so no need to return an error.
return nil
}
// 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 {
rpcLog.Infof("Accepting batch=%x", batch.ID[:])
// Let's store an event for each order in the batch that we did accept
// the batch.
if err := s.server.db.StoreBatchEvents(
batch, order.MatchStateAccepted, poolrpc.MatchRejectReason_NONE,
); err != nil {
rpcLog.Errorf("Unable to store order events: %v", err)
}
// Send the message to the server.
return s.auctioneer.SendAuctionMessage(&poolrpc.ClientAuctionMessage{
Msg: &poolrpc.ClientAuctionMessage_Accept{
Accept: &poolrpc.OrderMatchAccept{
BatchId: batch.ID[:],
},
},
})
}
// GetLsatTokens returns all tokens that are contained in the LSAT token store.
func (s *rpcServer) GetLsatTokens(_ context.Context,
_ *poolrpc.TokensRequest) (*poolrpc.TokensResponse, error) {
log.Infof("Get LSAT tokens request received")
tokens, err := s.server.lsatStore.AllTokens()
if err != nil {
return nil, err
}
rpcTokens := make([]*poolrpc.LsatToken, len(tokens))
idx := 0
for key, token := range tokens {
macBytes, err := token.BaseMacaroon().MarshalBinary()
if err != nil {
return nil, err
}
rpcTokens[idx] = &poolrpc.LsatToken{
BaseMacaroon: macBytes,
PaymentHash: token.PaymentHash[:],
PaymentPreimage: token.Preimage[:],
AmountPaidMsat: int64(token.AmountPaid),
RoutingFeePaidMsat: int64(token.RoutingFeePaid),
TimeCreated: token.TimeCreated.Unix(),
Expired: !token.IsValid(),
StorageName: key,
}
idx++
}
return &poolrpc.TokensResponse{Tokens: rpcTokens}, nil
}
// 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,
chanInfos map[wire.OutPoint]*chaninfo.ChannelInfo) error {
// Prepare the list of witness stacks and channel infos and send them to
// the server.
rpcSigs := make(map[string][]byte, len(sigs))
for acctKey, sig := range sigs {
key := hex.EncodeToString(acctKey[:])
rpcSigs[key] = sig.Serialize()
}
rpcChannelInfos := make(map[string]*poolrpc.ChannelInfo, len(chanInfos))
for chanPoint, chanInfo := range chanInfos {
var channelType poolrpc.ChannelType
switch chanInfo.Version {
case chanbackup.TweaklessCommitVersion:
channelType = poolrpc.ChannelType_TWEAKLESS
case chanbackup.AnchorsCommitVersion:
channelType = poolrpc.ChannelType_ANCHORS
default:
return fmt.Errorf("unknown channel type: %v",
chanInfo.Version)
}
rpcChannelInfos[chanPoint.String()] = &poolrpc.ChannelInfo{
Type: channelType,
LocalNodeKey: chanInfo.LocalNodeKey.
SerializeCompressed(),
RemoteNodeKey: chanInfo.RemoteNodeKey.
SerializeCompressed(),
LocalPaymentBasePoint: chanInfo.LocalPaymentBasePoint.
SerializeCompressed(),
RemotePaymentBasePoint: chanInfo.RemotePaymentBasePoint.
SerializeCompressed(),
}
}
rpcLog.Infof("Sending OrderMatchSign for batch %x", batch.ID[:])
// We've successfully processed the sign message, let's store an event
// for this for all orders that were involved on our side.
if err := s.server.db.StoreBatchEvents(
batch, order.MatchStateSigned, poolrpc.MatchRejectReason_NONE,
); err != nil {
rpcLog.Errorf("Unable to store order events: %v", err)
}
return s.auctioneer.SendAuctionMessage(&poolrpc.ClientAuctionMessage{
Msg: &poolrpc.ClientAuctionMessage_Sign{
Sign: &poolrpc.OrderMatchSign{
BatchId: batch.ID[:],
AccountSigs: rpcSigs,
ChannelInfos: rpcChannelInfos,
},
},
})
}
// AuctionFee returns the current fee rate charged for matched orders within
// the auction.
func (s *rpcServer) AuctionFee(ctx context.Context,
_ *poolrpc.AuctionFeeRequest) (*poolrpc.AuctionFeeResponse, error) {
terms, err := s.auctioneer.Terms(ctx)
if err != nil {
return nil, fmt.Errorf("unable to query auctioneer terms: %v",
err)
}
// TODO(roasbeef): accept the amt of order instead?
return &poolrpc.AuctionFeeResponse{
ExecutionFee: &poolrpc.ExecutionFee{
BaseFee: uint64(terms.OrderExecBaseFee),
FeeRate: uint64(terms.OrderExecFeeRate),
},
}, nil
}
// Leases returns the list of channels that were either purchased or sold by the
// trader within the auction.
func (s *rpcServer) Leases(ctx context.Context,
req *poolrpc.LeasesRequest) (*poolrpc.LeasesResponse, error) {
// We'll start by parsing the list of accounts we should retrieve lease
// for. If none are specified, leases for all accounts will be returned.
accounts := make(map[[33]byte]struct{}, len(req.Accounts))
for _, rawAccountKey := range req.Accounts {
_, err := btcec.ParsePubKey(rawAccountKey, btcec.S256())
if err != nil {
return nil, fmt.Errorf("invalid account key: %v", err)
}
var accountKey [33]byte
copy(accountKey[:], rawAccountKey)
accounts[accountKey] = struct{}{}
}
// We'll then retrieve the list of specified batches. If none are
// specified, we'll retrieve all of them.
var batches []*clientdb.LocalBatchSnapshot
if len(req.BatchIds) == 0 {
var err error
batches, err = s.server.db.GetLocalBatchSnapshots()
if err != nil {
return nil, err
}
} else {
for _, rawBatchID := range req.BatchIds {
batchKey, err := btcec.ParsePubKey(
rawBatchID, btcec.S256(),
)
if err != nil {
return nil, fmt.Errorf("invalid batch id: %v",
err)
}
batchID := order.NewBatchID(batchKey)
batch, err := s.server.db.GetLocalBatchSnapshot(batchID)
if err != nil {
return nil, err
}
batches = append(batches, batch)
}
}
// As the true lease expiry of a channel is only known after tit has
// been confirmed, we'll assemble a map from the outpoint of a channel
// to the thaw_height which is actually the lease expiry height.
openChans, err := s.lndClient.ListChannels(
context.Background(), &lnrpc.ListChannelsRequest{},
)
if err != nil {
return nil, fmt.Errorf("unable to fetch set of open "+
"channels: %v", err)
}
// We'll map the outpoint string representation of a channel to the
// actual lease expiry height which we'll use to partially populate our
// response.
chanLeaseExpiries := make(map[string]uint32)
for _, channel := range openChans.Channels {
if channel.ThawHeight == 0 {
continue
}
// The thaw height is relative for channels pre
// script-enforcement, so we'll add an offset from the
// confirmation height of the channel.
sid := lnwire.NewShortChanIDFromInt(channel.ChanId)
expiryHeight := channel.ThawHeight + sid.BlockHeight
chanLeaseExpiries[channel.ChannelPoint] = expiryHeight
}
return s.prepareLeasesResponse(
ctx, accounts, batches, chanLeaseExpiries,
)
}
// fetchNodeRatings returns an up to date set of ratings for each of the nodes
// we matched with in the passed batch.
func fetchNodeRatings(ctx context.Context, batches []*clientdb.LocalBatchSnapshot,
auctioneer *auctioneer.Client) (map[[33]byte]poolrpc.NodeTier, error) {
// As the node ratings call is batched, we'll first do a single query
// to fetch all the ratings that we'll want to populate below.
//
// TODO(roasbeef): capture rating at time of lease? also cache
nodeRatings := make(map[[33]byte]poolrpc.NodeTier)
for _, batch := range batches {
for _, matches := range batch.MatchedOrders {
for _, match := range matches {
nodeRatings[match.NodeKey] = 0
}
}
}
nodeKeys := make([]*btcec.PublicKey, 0, len(nodeRatings))
for pubKey := range nodeRatings {
nodeKey, err := btcec.ParsePubKey(
pubKey[:], btcec.S256(),
)
if err != nil {
return nil, err
}
nodeKeys = append(nodeKeys, nodeKey)
}
// Query for the latest node ranking information for each of the nodes
// we gathered above.
nodeRatingsResp, err := auctioneer.NodeRating(ctx, nodeKeys...)
if err != nil {
return nil, fmt.Errorf("unable to query "+
"node tier: %v", err)
}
for _, nodeRating := range nodeRatingsResp.NodeRatings {
var pubKey [33]byte
copy(pubKey[:], nodeRating.NodePubkey)
nodeRatings[pubKey] = nodeRating.NodeTier
}
return nodeRatings, nil
}
// prepareLeasesResponse prepares a poolrpc.LeasesResponse for the given
// accounts in the given batches.
func (s *rpcServer) prepareLeasesResponse(ctx context.Context,
accounts map[[33]byte]struct{}, batches []*clientdb.LocalBatchSnapshot,
chanLeaseExpiries map[string]uint32) (*poolrpc.LeasesResponse, error) {
// First, we'll try to grab our set of node ratings as we want them to
// fully populate the lease response.
nodeRatings, err := fetchNodeRatings(ctx, batches, s.auctioneer)
if err != nil {
return nil, fmt.Errorf("unable to fetch node ratings: %v", err)
}
// Now we're ready to prepare our response.
var (
rpcLeases []*poolrpc.Lease
totalAmtEarned btcutil.Amount
totalAmtPaid btcutil.Amount
)
for _, batch := range batches {
// Keep a tally of how many channels were created within each
// batch in order to estimate the chain fees paid.
numChans := 0
// Keep track of the index for the first lease found for this
// batch. We'll use this to know where to start from when
// populating the ChainFeeSat field for each lease.
firstIdxForBatch := len(rpcLeases)
for nonce, matches := range batch.MatchedOrders {
nonce := nonce
for _, match := range matches {
// Obtain our order that was matched.
ourOrder, ok := batch.Orders[nonce]
if !ok {
return nil, fmt.Errorf("order %v not "+
"found in batch snapshot", nonce)
}
// Filter out the account if required.
_, ok = accounts[ourOrder.Details().AcctKey]
if len(accounts) != 0 && !ok {
continue
}
// Derive the channel output script, which we'll
// use to locate the channel outpoint within the
// batch transaction.
ourMultiSigKey, err := s.server.lndServices.WalletKit.DeriveKey(
ctx, &ourOrder.Details().MultiSigKeyLocator,
)
if err != nil {
return nil, err
}
chanAmt := match.UnitsFilled.ToSatoshis()
_, chanOutput, err := input.GenFundingPkScript(
ourMultiSigKey.PubKey.SerializeCompressed(),
match.MultiSigKey[:], int64(chanAmt),
)
if err != nil {
return nil, err
}
batchTxHash := batch.BatchTX.TxHash()
chanOutputIdx, ok := poolscript.LocateOutputScript(
batch.BatchTX, chanOutput.PkScript,
)
if !ok {
return nil, fmt.Errorf("channel with "+
"script %x not found in batch "+
"transaction %v",
chanOutput.PkScript, batchTxHash)
}
// The duration of the channel is always that
// specified by the bid order.
var bidDuration uint32
if ourOrder.Type() == order.TypeBid {
bidDuration = ourOrder.(*order.Bid).LeaseDuration
} else {
bidDuration = match.Order.(*order.Bid).LeaseDuration
}
// Calculate the premium paid/received to/from
// the maker/taker and the execution fee paid
// to the auctioneer and tally them.
premium := batch.ClearingPrice.LumpSumPremium(
chanAmt, bidDuration,
)
exeFee := batch.ExecutionFee.BaseFee() +
batch.ExecutionFee.ExecutionFee(chanAmt)
if ourOrder.Type() == order.TypeBid {
totalAmtPaid += premium
} else {
totalAmtEarned += premium
}
totalAmtPaid += exeFee
chanPointStr := fmt.Sprintf(
"%v:%v", batchTxHash, chanOutputIdx,
)
leaseExpiryHeight := chanLeaseExpiries[chanPointStr]
purchased := ourOrder.Type() == order.TypeBid
nodeTier := nodeRatings[match.NodeKey]
rpcLeases = append(rpcLeases, &poolrpc.Lease{
ChannelPoint: &poolrpc.OutPoint{
Txid: batchTxHash[:],
OutputIndex: chanOutputIdx,
},
ChannelAmtSat: uint64(chanAmt),
ChannelDurationBlocks: bidDuration,
ChannelLeaseExpiry: leaseExpiryHeight,
PremiumSat: uint64(premium),
ClearingRatePrice: uint64(batch.ClearingPrice),
OrderFixedRate: uint64(ourOrder.Details().FixedRate),
ExecutionFeeSat: uint64(exeFee),
OrderNonce: nonce[:],
Purchased: purchased,
ChannelRemoteNodeKey: match.NodeKey[:],
ChannelNodeTier: nodeTier,
})
numChans++
}
// Estimate the chain fees paid for the number of
// channels created in this batch and tally them.
chainFee := order.EstimateTraderFee(
uint32(numChans), batch.BatchTxFeeRate,
)
// We'll need to compute the chain fee paid for each
// lease. Since multiple leases can be created within a
// batch, we'll need to evenly distribute. We'll always
// apply the remainder to the first lease in the batch.
chainFeePerChan := chainFee / btcutil.Amount(numChans)
chainFeePerChanRem := chainFee % btcutil.Amount(numChans)
for i := firstIdxForBatch; i < len(rpcLeases); i++ {
chainFee := chainFeePerChan
if i == firstIdxForBatch {
chainFee += chainFeePerChanRem
}
rpcLeases[i].ChainFeeSat = uint64(chainFee)
}
totalAmtPaid += chainFee
}
}
return &poolrpc.LeasesResponse{
Leases: rpcLeases,
TotalAmtEarnedSat: uint64(totalAmtEarned),
TotalAmtPaidSat: uint64(totalAmtPaid),
}, nil
}
// BatchSnapshot returns information about a target batch including the
// clearing price of the batch, and the set of orders matched within the batch.
func (s *rpcServer) BatchSnapshot(ctx context.Context,
req *poolrpc.BatchSnapshotRequest) (*poolrpc.BatchSnapshotResponse, error) {
// THe current version of this call just proxies the request to the
// auctioneer, as we may only have information about the batch if we
// were matched with it on disk.
var batchID order.BatchID
copy(batchID[:], req.BatchId)
return s.auctioneer.BatchSnapshot(ctx, batchID)
}
// LeaseDurations returns the current set of valid lease duration in the
// market as is, and also information w.r.t if the market is currently active.
func (s *rpcServer) LeaseDurations(ctx context.Context,
_ *poolrpc.LeaseDurationRequest) (*poolrpc.LeaseDurationResponse, error) {
terms, err := s.auctioneer.Terms(ctx)
if err != nil {
return nil, fmt.Errorf("unable to query auctioneer terms: %v",
err)
}
return &poolrpc.LeaseDurationResponse{
LeaseDurations: terms.LeaseDurations,
}, nil
}
// NextBatchInfo returns information about the next batch the auctioneer
// will perform.
func (s *rpcServer) NextBatchInfo(ctx context.Context,
_ *poolrpc.NextBatchInfoRequest) (*poolrpc.NextBatchInfoResponse, error) {
terms, err := s.auctioneer.Terms(ctx)
if err != nil {
return nil, fmt.Errorf("unable to query auctioneer terms: %v",
err)
}
return &poolrpc.NextBatchInfoResponse{
ConfTarget: terms.NextBatchConfTarget,
FeeRateSatPerKw: uint64(terms.NextBatchFeeRate),
ClearTimestamp: uint64(terms.NextBatchClear.Unix()),
}, nil
}
// NodeRatings returns rating information about the target node. This can be
// used to query the rating of your own node, or other nodes to determine which
// asks/bids might be filled based on a target min node tier.
func (s *rpcServer) NodeRatings(ctx context.Context,
req *poolrpc.NodeRatingRequest) (*poolrpc.NodeRatingResponse, error) {
if len(req.NodePubkeys) == 0 {
return nil, fmt.Errorf("node pub key must be provided")
}
pubKeys := make([]*btcec.PublicKey, 0, len(req.NodePubkeys))
for _, nodeKeyBytes := range req.NodePubkeys {
nodeKey, err := btcec.ParsePubKey(nodeKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
pubKeys = append(pubKeys, nodeKey)
}
return s.auctioneer.NodeRating(ctx, pubKeys...)
}
// rpcOrderStateToDBState maps the order state as received over the RPC
// protocol to the local state that we use in the database.
func rpcOrderStateToDBState(state poolrpc.OrderState) (order.State, error) {
switch state {
case poolrpc.OrderState_ORDER_SUBMITTED:
return order.StateSubmitted, nil
case poolrpc.OrderState_ORDER_CLEARED:
return order.StateCleared, nil
case poolrpc.OrderState_ORDER_PARTIALLY_FILLED:
return order.StatePartiallyFilled, nil
case poolrpc.OrderState_ORDER_EXECUTED:
return order.StateExecuted, nil
case poolrpc.OrderState_ORDER_CANCELED:
return order.StateCanceled, nil
case poolrpc.OrderState_ORDER_EXPIRED:
return order.StateExpired, nil
case poolrpc.OrderState_ORDER_FAILED:
return order.StateFailed, nil
default:
return 0, fmt.Errorf("unknown state: %v", state)
}
}
// dbOrderStateToRPCState maps the order state as stored in the database to the
// corresponding RPC enum type.
func dbOrderStateToRPCState(state order.State) (poolrpc.OrderState, error) {
switch state {
case order.StateSubmitted:
return poolrpc.OrderState_ORDER_SUBMITTED, nil
case order.StateCleared:
return poolrpc.OrderState_ORDER_CLEARED, nil
case order.StatePartiallyFilled:
return poolrpc.OrderState_ORDER_PARTIALLY_FILLED, nil
case order.StateExecuted:
return poolrpc.OrderState_ORDER_EXECUTED, nil
case order.StateCanceled:
return poolrpc.OrderState_ORDER_CANCELED, nil
case order.StateExpired:
return poolrpc.OrderState_ORDER_EXPIRED, nil
case order.StateFailed:
return poolrpc.OrderState_ORDER_FAILED, nil
default:
return 0, fmt.Errorf("unknown state: %v", state)
}
}
// dbMatchStateToRPCMatchState maps the match state as stored in the database to
// the corresponding RPC enum type.
func dbMatchStateToRPCMatchState(
matchState order.MatchState) (poolrpc.MatchState, error) {
switch matchState {
case order.MatchStatePrepare:
return poolrpc.MatchState_PREPARE, nil
case order.MatchStateAccepted:
return poolrpc.MatchState_ACCEPTED, nil
case order.MatchStateRejected:
return poolrpc.MatchState_REJECTED, nil
case order.MatchStateSigned:
return poolrpc.MatchState_SIGNED, nil
case order.MatchStateFinalized:
return poolrpc.MatchState_FINALIZED, nil
default:
return 0, fmt.Errorf("unknown match state: %v", matchState)
}
}
// dbEventsToRPCEvents maps the events as stored in the database to the
// corresponding RPC types.
func dbEventsToRPCEvents(dbEvents []event.Event) ([]*poolrpc.OrderEvent,
error) {
rpcEvents := make([]*poolrpc.OrderEvent, 0, len(dbEvents))
for _, record := range dbEvents {
rpcEvent := &poolrpc.OrderEvent{
TimestampNs: record.Timestamp().UnixNano(),
EventStr: record.String(),
}
// Decode by type to allow for further kinds of events.
switch e := record.(type) {
case *clientdb.CreatedEvent:
// No additional information other than the timestamp
// itself is stored for this event type.
case *clientdb.UpdatedEvent:
prevState, err := dbOrderStateToRPCState(e.PrevState)
if err != nil {
return nil, err
}
newState, err := dbOrderStateToRPCState(e.NewState)
if err != nil {
return nil, err
}
rpcEvent.Event = &poolrpc.OrderEvent_StateChange{
StateChange: &poolrpc.UpdatedEvent{
PreviousState: prevState,
NewState: newState,
UnitsFilled: uint32(e.UnitsFilled),
},
}
case *clientdb.MatchEvent:
matchState, err := dbMatchStateToRPCMatchState(
e.MatchState,
)
if err != nil {
return nil, err
}
rpcEvent.Event = &poolrpc.OrderEvent_Matched{
Matched: &poolrpc.MatchEvent{
MatchState: matchState,
UnitsFilled: uint32(e.UnitsFilled),
MatchedOrder: e.MatchedOrder[:],
RejectReason: poolrpc.MatchRejectReason(
e.RejectReason,
),
},
}
default:
return nil, fmt.Errorf("unknown event type: %v", e)
}
rpcEvents = append(rpcEvents, rpcEvent)
}
return rpcEvents, nil
}
// nodeHasTorAddrs returns true if there exists a Tor address amongst the set
// of active Uris for a node.
func nodeHasTorAddrs(nodeAddrs []string) bool {
for _, nodeAddr := range nodeAddrs {
// Obtain the host to determine if this is a Tor address.
host, _, err := net.SplitHostPort(nodeAddr)
if err != nil {
host = nodeAddr
}
if tor.IsOnionHost(host) {
return true
}
}
return false
}
// unmarshallNodeTier maps the RPC node tier enum to the node tier used in
// memory.
func unmarshallNodeTier(nodeTier poolrpc.NodeTier) (order.NodeTier, error) {
switch nodeTier {
case poolrpc.NodeTier_TIER_DEFAULT:
return order.DefaultMinNodeTier, nil
case poolrpc.NodeTier_TIER_0:
return order.NodeTier0, nil
case poolrpc.NodeTier_TIER_1:
return order.NodeTier1, nil
default:
return 0, fmt.Errorf("unknown node tier: %v", nodeTier)
}
}