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) 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, nonce) 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 } // 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) } }