server+funding: add sidecar offer to funding manager

With this commit we add the business logic for creating a sidecar
channel offer as the provider.
We choose the funding manager as the place for this logic as that
already has most of the required lnd clients and will ultimately be
responsible for kickstarting the funding of the sidecar channel on the
provider's side.
This commit is contained in:
Oliver Gugger 2021-03-19 11:50:06 +01:00
parent 27ed02a5c4
commit bc9bf56a8d
No known key found for this signature in database
GPG key ID: 8E4256593F177720
3 changed files with 146 additions and 4 deletions

View file

@ -11,6 +11,7 @@ import (
"sync/atomic"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
@ -19,7 +20,9 @@ import (
"github.com/lightninglabs/pool/chaninfo"
"github.com/lightninglabs/pool/clientdb"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/sidecar"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/subscribe"
@ -31,6 +34,13 @@ import (
)
var (
// nodeIdentityKeyLoc is the key locator from which the identity key of
// an lnd node is derived.
nodeIdentityKeyLoc = keychain.KeyLocator{
Family: keychain.KeyFamilyNodeKey,
Index: 0,
}
// rpcCodeFundingFailed is the error code we send if the channel funding
// fails because of a timeout or another problem.
rpcCodeFundingFailed = auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED
@ -106,10 +116,16 @@ type ManagerConfig struct {
// LightningClient is an lndclient wrapped lnrpc client.
LightningClient lndclient.LightningClient
// SignerClient is an lndclient wrapped signrpc client.
SignerClient lndclient.SignerClient
// BaseClient is a raw lnrpc client that implements all methods the
// funding manager needs.
BaseClient BaseClient
// NodePubKey is the connected lnd node's identity public key.
NodePubKey *btcec.PublicKey
// NewNodesOnly specifies if the funding manager should only accept
// matched orders with channels from new nodes that the connected lnd
// node doesn't already have channels with.
@ -864,6 +880,44 @@ func (m *Manager) RemovePendingBatchArtifacts(
return nil
}
// OfferSidecar creates a sidecar channel offer and embeds it in a new sidecar
// ticket. The offer is signed with the local lnd's node public key.
func (m *Manager) OfferSidecar(ctx context.Context, capacity,
pushAmt btcutil.Amount, duration uint32) (*sidecar.Ticket, error) {
// Make sure the capacity and push amounts are sane.
err := sidecar.CheckOfferParams(capacity, pushAmt, order.BaseSupplyUnit)
if err != nil {
return nil, err
}
// So far everything looks good. Let's create the ticket with the offer
// now.
ticket, err := sidecar.NewTicket(
sidecar.VersionDefault, capacity, pushAmt, duration,
m.cfg.NodePubKey,
)
if err != nil {
return nil, fmt.Errorf("error creating sidecar ticket: %v", err)
}
// Let's sign the offer part of the ticket with our node's identity key
// now.
if err := sidecar.SignOffer(
ctx, ticket, nodeIdentityKeyLoc, m.cfg.SignerClient,
); err != nil {
return nil, fmt.Errorf("error signing offer: %v", err)
}
// Let's now store and return the ticket with the signed offer.
err = m.cfg.DB.AddSidecar(ticket)
if err != nil {
return nil, fmt.Errorf("error storing sidecar ticket: %v", err)
}
return ticket, nil
}
// connectToMatchedTrader attempts to connect to a trader that we've had an
// order matched with, on all available addresses.
func (m *Manager) connectToMatchedTrader(ctx context.Context,

View file

@ -2,14 +2,17 @@ package funding
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"math/big"
"net"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
@ -240,6 +243,7 @@ type managerHarness struct {
quit chan struct{}
lnMock *test.MockLightning
baseClientMock *fundingBaseClientMock
signerMock *test.MockSigner
mgr *Manager
}
@ -256,6 +260,7 @@ func newManagerHarness(t *testing.T) *managerHarness {
quit := make(chan struct{})
lightningClient := test.NewMockLightning()
walletKitClient := test.NewMockWalletKit()
signerClient := test.NewMockSigner()
baseClientMock := &fundingBaseClientMock{
lightningClient: lightningClient,
fundingShims: make(map[[32]byte]*lnrpc.ChanPointShim),
@ -266,10 +271,16 @@ func newManagerHarness(t *testing.T) *managerHarness {
quit: quit,
}
mgr := NewManager(&ManagerConfig{
DB: db,
WalletKit: walletKitClient,
LightningClient: lightningClient,
BaseClient: baseClientMock,
DB: db,
WalletKit: walletKitClient,
LightningClient: lightningClient,
BaseClient: baseClientMock,
SignerClient: signerClient,
NodePubKey: &btcec.PublicKey{
Curve: btcec.S256(),
X: new(big.Int),
Y: new(big.Int),
},
NewNodesOnly: true,
BatchStepTimeout: 400 * time.Millisecond,
})
@ -284,6 +295,7 @@ func newManagerHarness(t *testing.T) *managerHarness {
quit: quit,
lnMock: lightningClient,
baseClientMock: baseClientMock,
signerMock: signerClient,
mgr: mgr,
}
}
@ -636,6 +648,80 @@ func TestWaitForPeerConnections(t *testing.T) {
require.Equal(t, expectedErr, err)
}
// TestOfferSidecarValidation checks the validation logic in the sidecar
// offering process.
func TestOfferSidecarValidation(t *testing.T) {
h := newManagerHarness(t)
defer h.stop()
negativeCases := []struct {
name string
capacity btcutil.Amount
pushAmt btcutil.Amount
expectedErr string
}{{
name: "empty capacity",
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid capacity",
capacity: 123,
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid push amount",
capacity: 100000,
pushAmt: 100001,
expectedErr: "self channel balance must be smaller than " +
"or equal to capacity",
}}
for _, testCase := range negativeCases {
_, err := h.mgr.OfferSidecar(
context.Background(), testCase.capacity,
testCase.pushAmt, 2016,
)
require.Error(t, err)
require.Contains(t, err.Error(), testCase.expectedErr)
}
}
// TestOfferSidecar makes sure sidecar offers can be created and signed with the
// lnd node's identity key.
func TestOfferSidecar(t *testing.T) {
h := newManagerHarness(t)
defer h.stop()
// We'll need a formally valid signature to pass the parsing. So we'll
// just create a dummy signature from a random key pair.
privKey, err := btcec.NewPrivateKey(btcec.S256())
require.NoError(t, err)
hash := sha256.New()
_, _ = hash.Write([]byte("foo"))
digest := hash.Sum(nil)
sig, err := privKey.Sign(digest)
require.NoError(t, err)
h.mgr.cfg.NodePubKey = privKey.PubKey()
h.signerMock.Signature = sig.Serialize()
var nodeKeyRaw [33]byte
copy(nodeKeyRaw[:], privKey.PubKey().SerializeCompressed())
// Let's create our offer now.
capacity, pushAmt := btcutil.Amount(100_000), btcutil.Amount(40_000)
ticket, err := h.mgr.OfferSidecar(
context.Background(), capacity, pushAmt, 2016,
)
require.NoError(t, err)
require.Equal(t, capacity, ticket.Offer.Capacity)
require.Equal(t, pushAmt, ticket.Offer.PushAmt)
require.Equal(t, privKey.PubKey(), ticket.Offer.SignPubKey)
require.Equal(t, sig, ticket.Offer.SigOfferDigest)
// Make sure the DB has the exact same ticket now.
dbTicket, err := h.db.Sidecar(ticket.ID, privKey.PubKey())
require.NoError(t, err)
require.Equal(t, ticket, dbTicket)
}
func newKitFromTemplate(nonce order.Nonce, tpl *order.Kit) order.Kit {
kit := order.NewKit(nonce)
kit.Version = tpl.Version

View file

@ -474,7 +474,9 @@ func (s *Server) setupClient() error {
DB: s.db,
WalletKit: s.lndServices.WalletKit,
LightningClient: s.lndServices.Client,
SignerClient: s.lndServices.Signer,
BaseClient: s.lndClient,
NodePubKey: nodePubKey,
BatchStepTimeout: order.DefaultBatchStepTimeout,
NewNodesOnly: s.cfg.NewNodesOnly,
NotifyShimCreated: channelAcceptor.ShimRegistered,