pool/funding/manager_test.go
Olaoluwa Osuntokun 177e84f35a
funding: fix stream handling issues in consumePendingOpenChannels
In this commit, we fix a few issues with the streaming handling:
  1. If it got an error on Recv() it would just spin for ever,
generating an endless stream of spammy logs.
  2. It never listened on the Done() context channel for the stream. So
if the `lnd` had actually hung up, it wouldn't detect that and would
just keep spinning.

To make things more graceful, we also add back off when we try to
reconnect.
2025-07-01 17:59:02 -07:00

936 lines
26 KiB
Go

package funding
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"net"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool/auctioneerrpc"
"github.com/lightninglabs/pool/chaninfo"
"github.com/lightninglabs/pool/clientdb"
"github.com/lightninglabs/pool/internal/test"
"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/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
testTimeout = 100 * time.Millisecond
)
var (
node1Key = [33]byte{2, 3, 4}
node2Key = [33]byte{3, 4, 5}
addr1, _ = net.ResolveTCPAddr("tcp4", "10.0.1.1:9735")
addr2, _ = net.ResolveTCPAddr("tcp4", "192.168.1.1:9735")
)
type openChannelStream struct {
lnrpc.Lightning_OpenChannelClient
updateChan chan *lnrpc.OpenStatusUpdate
quit chan struct{}
}
func (i *openChannelStream) Recv() (*lnrpc.OpenStatusUpdate, error) {
select {
case msg := <-i.updateChan:
return msg, nil
case <-i.quit:
return nil, context.Canceled
}
}
type peerEventStream struct {
lnrpc.Lightning_SubscribePeerEventsClient
updateChan chan *lnrpc.PeerEvent
ctx context.Context
cancelSubscription chan struct{}
quit chan struct{}
}
func (i *peerEventStream) Recv() (*lnrpc.PeerEvent, error) {
select {
case msg := <-i.updateChan:
return msg, nil
// To mimic the real GRPC client, we'll return an error status.
case <-i.ctx.Done():
return nil, status.Error(
codes.DeadlineExceeded, i.ctx.Err().Error(),
)
case <-i.cancelSubscription:
return nil, status.Error(
codes.Canceled, "subscription canceled",
)
case <-i.quit:
return nil, context.Canceled
}
}
type channelEventStream struct {
lnrpc.Lightning_SubscribeChannelEventsClient
updateChan chan *lnrpc.ChannelEventUpdate
ctx context.Context
cancelSubscription chan struct{}
quit chan struct{}
}
func (c *channelEventStream) Context() context.Context {
return c.ctx
}
func (c *channelEventStream) Recv() (*lnrpc.ChannelEventUpdate, error) {
select {
case msg := <-c.updateChan:
return msg, nil
// To mimic the real GRPC client, we'll return an error status.
case <-c.ctx.Done():
return nil, status.Error(
codes.DeadlineExceeded, c.ctx.Err().Error(),
)
case <-c.cancelSubscription:
return nil, status.Error(
codes.Canceled, "subscription canceled",
)
case <-c.quit:
return nil, context.Canceled
}
}
type fundingBaseClientMock struct {
lightningClient *test.MockLightning
fundingShims map[[32]byte]*lnrpc.ChanPointShim
peerList map[route.Vertex]string
peerEvents chan *lnrpc.PeerEvent
channelEvents chan *lnrpc.ChannelEventUpdate
cancelSub chan struct{}
quit chan struct{}
}
func (m *fundingBaseClientMock) FundingStateStep(_ context.Context,
req *lnrpc.FundingTransitionMsg,
_ ...grpc.CallOption) (*lnrpc.FundingStateStepResp, error) {
register := req.GetShimRegister()
if register == nil || register.GetChanPointShim() == nil {
return nil, fmt.Errorf("invalid funding shim")
}
var tempChanID [32]byte
copy(tempChanID[:], register.GetChanPointShim().PendingChanId)
m.fundingShims[tempChanID] = register.GetChanPointShim()
return nil, nil
}
func (m *fundingBaseClientMock) OpenChannel(ctx context.Context,
req *lnrpc.OpenChannelRequest,
_ ...grpc.CallOption) (lnrpc.Lightning_OpenChannelClient, error) {
if req.FundingShim == nil || req.FundingShim.GetChanPointShim() == nil {
return nil, fmt.Errorf("invalid funding shim")
}
var tempChanID [32]byte
copy(tempChanID[:], req.FundingShim.GetChanPointShim().PendingChanId)
_, ok := m.fundingShims[tempChanID]
if !ok {
return nil, fmt.Errorf("invalid funding shim")
}
node, err := route.NewVertexFromBytes(req.NodePubkey)
if err != nil {
return nil, err
}
op, _ := m.lightningClient.OpenChannel(
ctx, node, btcutil.Amount(req.LocalFundingAmount),
btcutil.Amount(req.PushSat), false,
)
stream := &openChannelStream{
quit: m.quit,
updateChan: make(chan *lnrpc.OpenStatusUpdate),
}
go func() {
select {
case stream.updateChan <- &lnrpc.OpenStatusUpdate{
Update: &lnrpc.OpenStatusUpdate_ChanPending{
ChanPending: &lnrpc.PendingUpdate{
Txid: op.Hash[:],
OutputIndex: op.Index,
},
},
}:
case <-m.quit:
}
}()
return stream, nil
}
func (m *fundingBaseClientMock) SubscribeChannelEvents(ctx context.Context,
_ *lnrpc.ChannelEventSubscription, _ ...grpc.CallOption) (
lnrpc.Lightning_SubscribeChannelEventsClient, error) {
return &channelEventStream{
quit: m.quit,
ctx: ctx,
updateChan: m.channelEvents,
}, nil
}
func (m *fundingBaseClientMock) ListPeers(_ context.Context,
_ *lnrpc.ListPeersRequest,
_ ...grpc.CallOption) (*lnrpc.ListPeersResponse, error) {
resp := &lnrpc.ListPeersResponse{}
for nodeKey, addr := range m.peerList {
resp.Peers = append(resp.Peers, &lnrpc.Peer{
PubKey: nodeKey.String(),
Address: addr,
})
}
return resp, nil
}
func (m *fundingBaseClientMock) SubscribePeerEvents(ctx context.Context,
_ *lnrpc.PeerEventSubscription, _ ...grpc.CallOption) (
lnrpc.Lightning_SubscribePeerEventsClient, error) {
return &peerEventStream{
quit: m.quit,
ctx: ctx,
cancelSubscription: m.cancelSub,
updateChan: m.peerEvents,
}, nil
}
func (m *fundingBaseClientMock) AbandonChannel(_ context.Context,
_ *lnrpc.AbandonChannelRequest,
_ ...grpc.CallOption) (*lnrpc.AbandonChannelResponse, error) {
return nil, nil
}
type managerHarness struct {
t *testing.T
tempDir string
db *clientdb.DB
quit chan struct{}
lnMock *test.MockLightning
baseClientMock *fundingBaseClientMock
signerMock *test.MockSigner
mgr *Manager
}
func newManagerHarness(t *testing.T) *managerHarness {
tempDir, err := ioutil.TempDir("", "client-db")
require.NoError(t, err)
db, err := clientdb.New(tempDir, clientdb.DBFilename)
if err != nil {
_ = os.RemoveAll(tempDir)
t.Fatalf("unable to create new db: %v", err)
}
quit := make(chan struct{})
lightningClient := test.NewMockLightning()
walletKitClient := test.NewMockWalletKit()
signerClient := test.NewMockSigner()
baseClientMock := &fundingBaseClientMock{
lightningClient: lightningClient,
fundingShims: make(map[[32]byte]*lnrpc.ChanPointShim),
peerList: make(map[route.Vertex]string),
peerEvents: make(chan *lnrpc.PeerEvent),
channelEvents: make(chan *lnrpc.ChannelEventUpdate),
cancelSub: make(chan struct{}),
quit: quit,
}
mgr := NewManager(&ManagerConfig{
DB: db,
WalletKit: walletKitClient,
LightningClient: lightningClient,
BaseClient: baseClientMock,
SignerClient: signerClient,
NodePubKey: btcec.NewPublicKey(
new(btcec.FieldVal).SetInt(0),
new(btcec.FieldVal).SetInt(0),
),
NewNodesOnly: true,
BatchStepTimeout: 400 * time.Millisecond,
})
err = mgr.Start()
require.NoError(t, err)
return &managerHarness{
t: t,
tempDir: tempDir,
db: db,
quit: quit,
lnMock: lightningClient,
baseClientMock: baseClientMock,
signerMock: signerClient,
mgr: mgr,
}
}
func (m *managerHarness) stop() {
close(m.quit)
require.NoError(m.t, m.db.Close())
require.NoError(m.t, os.RemoveAll(m.tempDir))
require.NoError(m.t, m.mgr.Stop())
}
// TestFundingManager tests that the two main steps of the funding manager (the
// channel funding preparation and the batch channel setup) are executed
// correctly. This involves checking the derived keys, funding shims and the
// established connections.
func TestFundingManager(t *testing.T) {
h := newManagerHarness(t)
defer h.stop()
// Set up a simple batch with an ask and a bid that are matched to each
// other twice, in each direction once. This is enough for our purposes
// as we mainly want to see the channels getting set up.
_, pubKeyAsk := test.CreateKey(0)
_, pubKeyBid := test.CreateKey(1)
ask := &order.Ask{
Kit: newKitFromTemplate(order.Nonce{0x01}, &order.Kit{
MultiSigKeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyTowerSession,
Index: 0,
},
Units: 4,
UnitsUnfulfilled: 4,
FixedRate: 10000,
LeaseDuration: 2500,
}),
}
err := h.db.SubmitOrder(ask)
require.NoError(t, err)
bid := &order.Bid{
Kit: newKitFromTemplate(order.Nonce{0x02}, &order.Kit{
MultiSigKeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyTowerSession,
Index: 1,
},
Units: 4,
UnitsUnfulfilled: 4,
FixedRate: 10000,
LeaseDuration: 2500,
}),
}
err = h.db.SubmitOrder(bid)
require.NoError(t, err)
matchedAsk := &order.MatchedOrder{
Order: ask,
UnitsFilled: 4,
MultiSigKey: [33]byte{2, 3, 4},
NodeKey: node1Key,
NodeAddrs: []net.Addr{addr1},
}
matchedBid := &order.MatchedOrder{
Order: bid,
UnitsFilled: 4,
MultiSigKey: [33]byte{3, 4, 5},
NodeKey: node2Key,
NodeAddrs: []net.Addr{addr2},
}
_, fundingOutput1, _ := input.GenFundingPkScript(
pubKeyAsk.SerializeCompressed(),
matchedBid.MultiSigKey[:], int64(4*order.BaseSupplyUnit),
)
_, fundingOutput2, _ := input.GenFundingPkScript(
pubKeyBid.SerializeCompressed(),
matchedAsk.MultiSigKey[:], int64(4*order.BaseSupplyUnit),
)
batchTx := &wire.MsgTx{
TxOut: []*wire.TxOut{fundingOutput1, fundingOutput2},
}
batch := &order.Batch{
ID: order.BatchID{9, 8, 7},
MatchedOrders: map[order.Nonce][]*order.MatchedOrder{
ask.Nonce(): {matchedBid},
bid.Nonce(): {matchedAsk},
},
BatchTX: batchTx,
}
pendingChanID := order.PendingChanKey(ask.Nonce(), bid.Nonce())
// Make sure the channel preparations work as expected. We expect the
// bidder to connect out to the asker so the bidder is waiting for a
// connection to the asker's peer to be established.
h.baseClientMock.peerList = map[route.Vertex]string{
node1Key: "1.1.1.1",
}
err = h.mgr.PrepChannelFunding(batch, h.db.GetOrder)
require.NoError(t, err)
// Verify we have the expected connections and funding shims registered.
// We expect the bidder to connect to the asker and having registered
// the funding shim while the asker is opening the channel.
require.Eventually(t, func() bool {
return len(h.lnMock.Connections()) == 1
}, testTimeout, testTimeout/10)
conns := h.lnMock.Connections()
require.Equal(t, 1, len(conns))
require.Equal(t, addr1.String(), conns[node1Key])
require.Equal(t, 1, len(h.baseClientMock.fundingShims))
// Validate the shim.
shim := h.baseClientMock.fundingShims[pendingChanID]
require.NotNil(t, shim)
assertFundingShim(
t, shim, uint32(2500), int64(order.SupplyUnit(4).ToSatoshis()),
batchTx, 1, &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: bid.Kit.MultiSigKeyLocator.Family,
Index: bid.Kit.MultiSigKeyLocator.Index,
},
PubKey: pubKeyBid,
}, matchedAsk.MultiSigKey[:])
// Next, make sure we get a partial reject error if we enable the "new
// nodes only" flag and already have a channel with the matched node.
h.mgr.cfg.NewNodesOnly = true
h.lnMock.Channels = append(h.lnMock.Channels, lndclient.ChannelInfo{
PubKeyBytes: node1Key,
})
err = h.mgr.PrepChannelFunding(batch, h.db.GetOrder)
require.Error(t, err)
expectedErr := &MatchRejectErr{
RejectedOrders: map[order.Nonce]*auctioneerrpc.OrderReject{
ask.Nonce(): {
ReasonCode: auctioneerrpc.OrderReject_DUPLICATE_PEER,
Reason: "already have open/pending channel " +
"with peer",
},
},
}
require.Equal(t, expectedErr, err)
// As a last check of the funding preparation, make sure we get a reject
// error if the connections to the remote peers couldn't be established.
h.mgr.cfg.NewNodesOnly = false
h.baseClientMock.peerList = make(map[route.Vertex]string)
err = h.mgr.PrepChannelFunding(batch, h.db.GetOrder)
require.Error(t, err)
expectedErr = &MatchRejectErr{
RejectedOrders: map[order.Nonce]*auctioneerrpc.OrderReject{
ask.Nonce(): {
ReasonCode: auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED,
Reason: "connection not established before " +
"timeout",
},
},
}
require.Equal(t, expectedErr, err)
// With everything set up, let's now test the normal and sidecar batch
// channel setup.
h.lnMock.ScbKeyRing.EncryptionKey.PubKey = pubKeyAsk
callBatchChannelSetup(t, h, batch, false)
callBatchChannelSetup(t, h, batch, true)
// Finally, make sure we get a timeout error if no channel open messages
// are received.
_, err = h.mgr.BatchChannelSetup(batch)
require.Error(t, err)
code := &auctioneerrpc.OrderReject{
ReasonCode: auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED,
Reason: "timed out waiting for pending open " +
"channel notification",
}
expectedErr = &MatchRejectErr{
RejectedOrders: map[order.Nonce]*auctioneerrpc.OrderReject{
ask.Nonce(): code,
bid.Nonce(): code,
},
}
require.Equal(t, expectedErr, err)
// Make sure the sidecar setup also times out if no updates come in.
_, err = h.mgr.SidecarBatchChannelSetup(
batch, h.mgr.pendingOpenChanClient, h.db.GetOrder,
)
require.Error(t, err)
require.Equal(t, expectedErr, err)
}
func callBatchChannelSetup(t *testing.T, h *managerHarness, batch *order.Batch,
sidecar bool) {
txidHash := batch.BatchTX.TxHash()
// Next, make sure we can complete the channel funding by opening the
// channel for which we are the bidder. We'll also expect a channel open
// message for the one where we are the asker so we simulate two msgs.
go func() {
timeout := time.After(time.Second)
msg := &lnrpc.ChannelEventUpdate{
Channel: &lnrpc.ChannelEventUpdate_PendingOpenChannel{
PendingOpenChannel: &lnrpc.PendingUpdate{
Txid: txidHash[:],
OutputIndex: 0,
},
},
}
// Send the message for the first channel.
select {
case h.baseClientMock.channelEvents <- msg:
case <-timeout:
}
// And again for the second channel.
msg2 := &lnrpc.ChannelEventUpdate{
Channel: &lnrpc.ChannelEventUpdate_PendingOpenChannel{
PendingOpenChannel: &lnrpc.PendingUpdate{
Txid: txidHash[:],
OutputIndex: 1,
},
},
}
select {
case h.baseClientMock.channelEvents <- msg2:
case <-timeout:
}
}()
// We need to fake channel backups as well. The mock creates them from
// the open channels, so let's add two of those.
h.lnMock.Channels = append(h.lnMock.Channels, lndclient.ChannelInfo{
ChannelPoint: fmt.Sprintf("%s:0", txidHash.String()),
})
h.lnMock.Channels = append(h.lnMock.Channels, lndclient.ChannelInfo{
ChannelPoint: fmt.Sprintf("%s:1", txidHash.String()),
})
var (
chanInfo map[wire.OutPoint]*chaninfo.ChannelInfo
err error
)
if sidecar {
chanInfo, err = h.mgr.SidecarBatchChannelSetup(
batch, h.mgr.pendingOpenChanClient, h.db.GetOrder,
)
} else {
chanInfo, err = h.mgr.BatchChannelSetup(batch)
}
require.NoError(t, err)
require.Equal(t, 2, len(chanInfo))
}
// TestDeriveFundingShim makes sure the correct keys are used for creating a
// funding shim.
func TestDeriveFundingShim(t *testing.T) {
h := newManagerHarness(t)
defer h.stop()
var (
bidKeyIndex int32 = 101
sidecarKeyIndex int32 = 102
_, pubKeyAsk = test.CreateKey(100)
_, pubKeyBid = test.CreateKey(bidKeyIndex)
_, pubKeySidecar = test.CreateKey(sidecarKeyIndex)
askNonce = order.Nonce{1, 2, 3}
bidNonce = order.Nonce{3, 2, 1}
expectedKeyLocator = keychain.KeyLocator{
Family: 1234,
Index: uint32(bidKeyIndex),
}
expectedKeyLocatorSidecar = keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: uint32(sidecarKeyIndex),
}
batchTx = &wire.MsgTx{
TxOut: []*wire.TxOut{{}},
}
batchHeightHint uint32 = 1337
)
askKit := order.NewKit(askNonce)
matchedAsk := &order.MatchedOrder{
Order: &order.Ask{Kit: *askKit},
UnitsFilled: 4,
}
copy(matchedAsk.MultiSigKey[:], pubKeyAsk.SerializeCompressed())
// First test is a normal bid where the funding key should be derived
// from the wallet
bid := &order.Bid{
Kit: newKitFromTemplate(bidNonce, &order.Kit{
MultiSigKeyLocator: expectedKeyLocator,
Units: 4,
LeaseDuration: 12345,
}),
}
_, batchTx.TxOut[0], _ = input.GenFundingPkScript(
pubKeyBid.SerializeCompressed(),
matchedAsk.MultiSigKey[:], int64(4*order.BaseSupplyUnit),
)
shim, pendingChanID, err := h.mgr.deriveFundingShim(
bid, matchedAsk, batchTx, batchHeightHint,
)
require.NoError(t, err)
require.Equal(t, order.PendingChanKey(askNonce, bidNonce), pendingChanID)
require.IsType(t, shim.Shim, &lnrpc.FundingShim_ChanPointShim{})
expectedKeyDescriptor := &keychain.KeyDescriptor{
PubKey: pubKeyBid,
KeyLocator: expectedKeyLocator,
}
chanPointShim := shim.Shim.(*lnrpc.FundingShim_ChanPointShim)
assertFundingShim(
t, chanPointShim.ChanPointShim, 12345, 400_000, batchTx, 0,
expectedKeyDescriptor, pubKeyAsk.SerializeCompressed(),
)
// And the second test is with a sidecar channel bid.
ticket, err := sidecar.NewTicket(
400_000, 0, 12345, pubKeyBid, false, false, false,
)
require.NoError(t, err)
ticket.Recipient = &sidecar.Recipient{
MultiSigPubKey: pubKeySidecar,
MultiSigKeyIndex: uint32(sidecarKeyIndex),
}
bid = &order.Bid{
Kit: newKitFromTemplate(bidNonce, &order.Kit{
MultiSigKeyLocator: expectedKeyLocator,
Units: 4,
LeaseDuration: 12345,
}),
SidecarTicket: ticket,
}
_, batchTx.TxOut[0], _ = input.GenFundingPkScript(
pubKeySidecar.SerializeCompressed(),
matchedAsk.MultiSigKey[:], int64(4*order.BaseSupplyUnit),
)
shim, pendingChanID, err = h.mgr.deriveFundingShim(
bid, matchedAsk, batchTx, batchHeightHint,
)
require.NoError(t, err)
require.Equal(t, order.PendingChanKey(askNonce, bidNonce), pendingChanID)
require.IsType(t, shim.Shim, &lnrpc.FundingShim_ChanPointShim{})
expectedKeyDescriptor = &keychain.KeyDescriptor{
PubKey: pubKeySidecar,
KeyLocator: expectedKeyLocatorSidecar,
}
chanPointShim = shim.Shim.(*lnrpc.FundingShim_ChanPointShim)
assertFundingShim(
t, chanPointShim.ChanPointShim, 12345, 400_000, batchTx, 0,
expectedKeyDescriptor, pubKeyAsk.SerializeCompressed(),
)
}
// assertFundingShim asserts that the funding shim contains the data that we
// expect.
func assertFundingShim(t *testing.T, shim *lnrpc.ChanPointShim,
thawHeight uint32, amt int64, fundingTx *wire.MsgTx,
fundingOutputIndex uint32, localKey *keychain.KeyDescriptor,
remoteKey []byte) {
require.Equal(t, thawHeight, shim.ThawHeight)
require.Equal(t, amt, shim.Amt)
fundingTxHash := fundingTx.TxHash()
chanPoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: fundingTxHash[:],
},
OutputIndex: fundingOutputIndex,
}
require.Equal(t, chanPoint, shim.ChanPoint)
require.Equal(
t, localKey.PubKey.SerializeCompressed(),
shim.LocalKey.RawKeyBytes,
)
require.Equal(t, int32(localKey.Family), shim.LocalKey.KeyLoc.KeyFamily)
require.Equal(t, int32(localKey.Index), shim.LocalKey.KeyLoc.KeyIndex)
require.Equal(t, remoteKey, shim.RemoteKey)
}
// TestWaitForPeerConnections tests the function that waits for peer connections
// to be established before continuing with the funding process.
func TestWaitForPeerConnections(t *testing.T) {
h := newManagerHarness(t)
defer h.stop()
// First we make sure that if all peer are already connected, no peer
// subscription is created.
h.baseClientMock.peerList = map[route.Vertex]string{
node1Key: "1.1.1.1",
node2Key: "2.2.2.2",
}
ctxt, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
expectedConnections := map[route.Vertex]struct{}{
node1Key: {},
node2Key: {},
}
err := h.mgr.waitForPeerConnections(ctxt, expectedConnections, nil)
require.NoError(t, err)
// Next, make sure that connections established while waiting are
// notified correctly. We simulate one connection already being done and
// one finishing while we wait.
h.baseClientMock.peerList = map[route.Vertex]string{
node1Key: "1.1.1.1",
}
go func() {
// Send an offline event first just to make sure it's consumed
// but ignored.
h.baseClientMock.peerEvents <- &lnrpc.PeerEvent{
PubKey: hex.EncodeToString(node2Key[:]),
Type: lnrpc.PeerEvent_PEER_OFFLINE,
}
// Now send the event that the manager is waiting for.
h.baseClientMock.peerEvents <- &lnrpc.PeerEvent{
PubKey: hex.EncodeToString(node2Key[:]),
Type: lnrpc.PeerEvent_PEER_ONLINE,
}
}()
ctxt, cancel = context.WithTimeout(context.Background(), testTimeout)
defer cancel()
expectedConnections = map[route.Vertex]struct{}{
node1Key: {},
node2Key: {},
}
err = h.mgr.waitForPeerConnections(ctxt, expectedConnections, nil)
require.NoError(t, err)
// Final test, make sure we get the correct error message back if we
// run into a timeout or context cancellation when waiting for the
// second node to be connected.
fakeNonce1, fakeNonce2 := order.Nonce{77, 88}, order.Nonce{88, 99}
fakeBatch := &order.Batch{
MatchedOrders: map[order.Nonce][]*order.MatchedOrder{
fakeNonce1: {{
NodeKey: node1Key,
Order: &order.Ask{
Kit: newKitFromTemplate(
fakeNonce1, &order.Kit{},
),
},
}},
fakeNonce2: {{
NodeKey: node2Key,
Order: &order.Ask{
Kit: newKitFromTemplate(
fakeNonce2, &order.Kit{},
),
},
}},
},
}
// Trigger a deadline exceeded error after a short timeout.
ctxt, cancel = context.WithTimeout(context.Background(), testTimeout)
defer cancel()
expectedConnections = map[route.Vertex]struct{}{
node1Key: {},
node2Key: {},
}
err = h.mgr.waitForPeerConnections(ctxt, expectedConnections, fakeBatch)
require.Error(t, err)
code := &auctioneerrpc.OrderReject{
ReasonCode: auctioneerrpc.OrderReject_CHANNEL_FUNDING_FAILED,
Reason: "connection not established before timeout",
}
expectedErr := &MatchRejectErr{
RejectedOrders: map[order.Nonce]*auctioneerrpc.OrderReject{
fakeNonce2: code,
},
}
require.Equal(t, expectedErr, err)
// Do the same again, but now trigger a context cancel. We make the
// context expire afterwards, since that will trigger the actual error
// return.
ctxt, cancel = context.WithTimeout(context.Background(), 2*testTimeout)
defer cancel()
go func() {
time.Sleep(testTimeout)
close(h.baseClientMock.cancelSub)
}()
err = h.mgr.waitForPeerConnections(ctxt, expectedConnections, fakeBatch)
require.Error(t, err)
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
bid *order.Bid
expectedErr string
}{{
name: "empty capacity",
bid: &order.Bid{},
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid capacity",
capacity: 123,
bid: &order.Bid{},
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid push amount",
capacity: 100000,
pushAmt: 100001,
bid: &order.Bid{},
expectedErr: "self channel balance must be smaller than " +
"or equal to capacity",
}, {
name: "no sidecar for outbound market",
capacity: 100000,
pushAmt: 0,
bid: &order.Bid{
Kit: order.Kit{
AuctionType: order.BTCOutboundLiquidity,
},
},
expectedErr: "market does not support sidecar tickets",
}}
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
desc := &keychain.KeyDescriptor{
PubKey: privKey.PubKey(),
}
// We'll need a formally valid signature to pass the parsing. So we'll
// just create a dummy signature from a random key pair.
hash := sha256.New()
_, _ = hash.Write([]byte("foo"))
digest := hash.Sum(nil)
sig := ecdsa.Sign(privKey, digest)
h.mgr.cfg.NodePubKey = privKey.PubKey()
h.signerMock.Signature = sig.Serialize()
var nodeKeyRaw [33]byte
copy(nodeKeyRaw[:], privKey.PubKey().SerializeCompressed())
for _, testCase := range negativeCases {
_, err := h.mgr.OfferSidecar(
context.Background(), testCase.capacity,
testCase.pushAmt, 2016, desc, testCase.bid, false,
)
fmt.Println(testCase.expectedErr)
if testCase.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), testCase.expectedErr)
continue
}
require.NoError(t, err)
}
}
// 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()
require.NoError(t, err)
hash := sha256.New()
_, _ = hash.Write([]byte("foo"))
digest := hash.Sum(nil)
sig := ecdsa.Sign(privKey, digest)
h.mgr.cfg.NodePubKey = privKey.PubKey()
h.signerMock.Signature = sig.Serialize()
var nodeKeyRaw [33]byte
copy(nodeKeyRaw[:], privKey.PubKey().SerializeCompressed())
bid := &order.Bid{
UnannouncedChannel: true,
ZeroConfChannel: true,
}
// 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,
&keychain.KeyDescriptor{
PubKey: privKey.PubKey(),
}, bid, false,
)
require.NoError(t, err)
require.Equal(t, capacity, ticket.Offer.Capacity)
require.Equal(t, pushAmt, ticket.Offer.PushAmt)
require.True(t, ticket.Offer.UnannouncedChannel)
require.True(t, ticket.Offer.ZeroConfChannel)
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
kit.State = tpl.State
kit.FixedRate = tpl.FixedRate
kit.Amt = tpl.Amt
kit.Units = tpl.Units
kit.UnitsUnfulfilled = tpl.UnitsUnfulfilled
kit.MultiSigKeyLocator = tpl.MultiSigKeyLocator
kit.MaxBatchFeeRate = tpl.MaxBatchFeeRate
kit.AcctKey = tpl.AcctKey
kit.LeaseDuration = tpl.LeaseDuration
return *kit
}