diff --git a/funding/manager.go b/funding/manager.go index 302fd49..7b7163d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -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, diff --git a/funding/manager_test.go b/funding/manager_test.go index 1111c44..3d8d06e 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -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 diff --git a/server.go b/server.go index 92ae3df..ade27c4 100644 --- a/server.go +++ b/server.go @@ -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,