From 882aec704d449d055ead92fdd4ffb00fcf8fc033 Mon Sep 17 00:00:00 2001 From: Andrey Samokhvalov Date: Tue, 2 May 2017 02:29:30 +0300 Subject: [PATCH] htlcswitch: add channel link tests Step #5 in making htlcManager (aka channelLink) testable: Combine all that have been done so far and add test framework for channel links which allow unit test: * message ordering * detect redundant messages * single hop payment * multihop payment * several cancel payment scenarios --- htlcswitch/link_test.go | 596 +++++++++++++++++++++++++++++++++++++++ htlcswitch/mock.go | 194 ++++++++++++- htlcswitch/test_utils.go | 507 +++++++++++++++++++++++++++++++++ 3 files changed, 1290 insertions(+), 7 deletions(-) create mode 100644 htlcswitch/link_test.go create mode 100644 htlcswitch/test_utils.go diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go new file mode 100644 index 000000000..45c4c0c05 --- /dev/null +++ b/htlcswitch/link_test.go @@ -0,0 +1,596 @@ +package htlcswitch + +import ( + "bytes" + "fmt" + "testing" + "time" + + "reflect" + + "io" + + "github.com/davecgh/go-spew/spew" + "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcutil" +) + +// messageToString is used to produce less spammy log messages in trace +// mode by setting the 'Curve" parameter to nil. Doing this avoids printing out +// each of the field elements in the curve parameters for secp256k1. +func messageToString(msg lnwire.Message) string { + switch m := msg.(type) { + case *lnwire.RevokeAndAck: + m.NextRevocationKey.Curve = nil + case *lnwire.NodeAnnouncement: + m.NodeID.Curve = nil + case *lnwire.ChannelAnnouncement: + m.NodeID1.Curve = nil + m.NodeID2.Curve = nil + m.BitcoinKey1.Curve = nil + m.BitcoinKey2.Curve = nil + case *lnwire.SingleFundingComplete: + m.RevocationKey.Curve = nil + case *lnwire.SingleFundingRequest: + m.CommitmentKey.Curve = nil + m.ChannelDerivationPoint.Curve = nil + case *lnwire.SingleFundingResponse: + m.ChannelDerivationPoint.Curve = nil + m.CommitmentKey.Curve = nil + m.RevocationKey.Curve = nil + } + + return spew.Sdump(msg) +} + +// createLogFunc is a helper function which returns the function which will be +// used for logging message are received from another peer. +func createLogFunc(name string, channelID lnwire.ChannelID) messageInterceptor { + return func(m lnwire.Message) { + if getChanID(m) == channelID { + // Skip logging of extend revocation window messages. + switch m := m.(type) { + case *lnwire.RevokeAndAck: + var zeroHash chainhash.Hash + if bytes.Equal(zeroHash[:], m.Revocation[:]) { + return + } + } + + fmt.Printf("---------------------- \n %v received: "+ + "%v", name, messageToString(m)) + } + } +} + +// TestChannelLinkSingleHopPayment in this test we checks the interaction +// between Alice and Bob within scope of one channel. +func TestChannelLinkSingleHopPayment(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatal(err) + } + defer n.stop() + + bobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + debug := false + if debug { + // Log message that alice receives. + n.aliceServer.record(createLogFunc("alice", + n.aliceChannelLink.ChanID())) + + // Log message that bob receives. + n.bobServer.record(createLogFunc("bob", + n.firstBobChannelLink.ChanID())) + } + + // Wait for: + // * HTLC add request to be sent to bob. + // * alice<->bob commitment state to be updated. + // * settle request to be sent back from bob to alice. + // * alice<->bob commitment state to be updated. + // * user notification to be sent. + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + invoice, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + }, amount) + if err != nil { + t.Fatalf("unable to make the payment: %v", err) + } + + // Wait for Bob to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that alice invoice was settled and bandwidth of HTLC + // links was changed. + if !invoice.Terms.Settled { + t.Fatal("invoice wasn't settled") + } + + if aliceBandwidthBefore-amount != n.aliceChannelLink.Bandwidth() { + t.Fatal("alice bandwidth should have descreased on payment " + + "amount") + } + + if bobBandwidthBefore+amount != n.firstBobChannelLink.Bandwidth() { + t.Fatal("bob bandwidth isn't match") + } +} + +// TestChannelLinkMultiHopPayment checks the ability to send payment over two +// hopes. In this test we send the payment from Carol to Alice over Bob peer. +// (Carol -> Bob -> Alice) and checking that HTLC was settled properly and +// balances were changed in two channels. +func TestChannelLinkMultiHopPayment(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatal(err) + } + defer n.stop() + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + debug := false + if debug { + // Log messages that alice receives from bob. + n.aliceServer.record(createLogFunc("[alice]<-bob<-carol: ", + n.aliceChannelLink.ChanID())) + + // Log messages that bob receives from alice. + n.bobServer.record(createLogFunc("alice->[bob]->carol: ", + n.firstBobChannelLink.ChanID())) + + // Log messages that bob receives from carol. + n.bobServer.record(createLogFunc("alice<-[bob]<-carol: ", + n.secondBobChannelLink.ChanID())) + + // Log messages that carol receives from bob. + n.carolServer.record(createLogFunc("alice->bob->[carol]", + n.carolChannelLink.ChanID())) + } + + // Wait for: + // * HTLC add request to be sent from Alice to Bob. + // * Alice<->Bob commitment states to be updated. + // * HTLC add request to be propagated to Carol. + // * Bob<->Carol commitment state to be updated. + // * settle request to be sent back from Carol to Bob. + // * Alice<->Bob commitment state to be updated. + // * settle request to be sent back from Bob to Alice. + // * Alice<->Bob commitment states to be updated. + // * user notification to be sent. + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + invoice, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + n.carolServer, + }, amount) + if err != nil { + t.Fatalf("unable to send payment: %v", err) + } + + // Wait for Bob to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that Carol invoice was settled and bandwidth of HTLC + // links were changed. + if !invoice.Terms.Settled { + t.Fatal("alice invoice wasn't settled") + } + + if aliceBandwidthBefore-amount != n.aliceChannelLink.Bandwidth() { + t.Fatal("the bandwidth of alice channel link which handles " + + "alice->bob channel wasn't decreased on htlc amount") + } + + if firstBobBandwidthBefore+amount != n.firstBobChannelLink.Bandwidth() { + t.Fatal("the bandwidth of bob channel link which handles " + + "alice->bob channel wasn't increased on htlc amount") + } + + if secondBobBandwidthBefore-amount != n.secondBobChannelLink.Bandwidth() { + t.Fatal("the bandwidth of bob channel link which handles " + + "bob->carol channel wasn't decreased on htlc amount") + } + + if carolBandwidthBefore+amount != n.carolChannelLink.Bandwidth() { + t.Fatal("the bandwidth of carol channel link which handles " + + "carol->bob channel wasn't decreased on htlc amount") + } +} + +// TestChannelLinkMultiHopInsufficientPayment checks that we receive error if +// bob<->alice channel has insufficient BTC capacity/bandwidth. In this test we +// send the payment from Carol to Alice over Bob peer. (Carol -> Bob -> Alice) +func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatalf("can't start three hop network: %v", err) + } + defer n.stop() + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + // Wait for: + // * HTLC add request to be sent to from Alice to Bob. + // * Alice<->Bob commitment states to be updated. + // * Bob trying to add HTLC add request in Bob<->Carol channel. + // * Cancel HTLC request to be sent back from Bob to Alice. + // * user notification to be sent. + var amount btcutil.Amount = 4 * btcutil.SatoshiPerBitcoin + invoice, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + n.carolServer, + }, amount) + if err == nil { + t.Fatal("error haven't been received") + } else if err.Error() != errors.New(lnwire.InsufficientCapacity).Error() { + t.Fatalf("wrong error have been received: %v", err) + } + + // Wait for Alice to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that alice invoice wasn't settled and bandwidth of htlc + // links hasn't been changed. + if invoice.Terms.Settled { + t.Fatal("alice invoice was settled") + } + + if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore { + t.Fatal("the bandwidth of alice channel link which handles " + + "alice->bob channel should be the same") + } + + if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "alice->bob channel should be the same") + } + + if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "bob->carol channel should be the same") + } + + if n.carolChannelLink.Bandwidth() != carolBandwidthBefore { + t.Fatal("the bandwidth of carol channel link which handles " + + "bob->carol channel should be the same") + } + +} + +// TestChannelLinkMultiHopUnknownPaymentHash checks that we receive remote error +// from Alice if she received not suitable payment hash for htlc. +func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatalf("can't start three hop network: %v", err) + } + defer n.stop() + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + + // Generate route convert it to blob, and return next destination for + // htlc add request. + peers := []Peer{ + n.bobServer, + n.carolServer, + } + firstNode, blob, err := generateRoute(peers) + if err != nil { + t.Fatal(err) + } + + // Generate payment: invoice and htlc. + invoice, htlc, err := generatePayment(amount, blob) + if err != nil { + t.Fatal(err) + } + + // We need to have wrong rhash for that reason we should change the + // preimage. Inverse first byte by xoring with 0xff. + invoice.Terms.PaymentPreimage[0] ^= byte(255) + + // Check who is last in the route and add invoice to server registry. + if err := n.carolServer.registry.AddInvoice(invoice); err != nil { + t.Fatalf("can't add invoice in carol registry: %v", err) + } + + // Send payment and expose err channel. + if _, err := n.aliceServer.htlcSwitch.SendHTLC(firstNode, + htlc); err == nil { + t.Fatal("error wasn't received") + } + + // Wait for Alice to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that alice invoice wasn't settled and bandwidth of htlc + // links hasn't been changed. + if invoice.Terms.Settled { + t.Fatal("alice invoice was settled") + } + + if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore { + t.Fatal("the bandwidth of alice channel link which handles " + + "alice->bob channel should be the same") + } + + if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "alice->bob channel should be the same") + } + + if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "bob->carol channel should be the same") + } + + if n.carolChannelLink.Bandwidth() != carolBandwidthBefore { + t.Fatal("the bandwidth of carol channel link which handles " + + "bob->carol channel should be the same") + } +} + +// TestChannelLinkMultiHopUnknownNextHop construct the chain of hops +// Carol<->Bob<->Alice and checks that we receive remote error from Bob if he +// has no idea about next hop (hop might goes down and routing info not updated +// yet) +func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatal(err) + } + defer n.stop() + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + + dave := newMockServer(t, "save") + invoice, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + dave, + }, amount) + if err == nil { + t.Fatal("error haven't been received") + } else if err.Error() != errors.New(lnwire.UnknownDestination).Error() { + t.Fatalf("wrong error have been received: %v", err) + } + + // Wait for Alice to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that alice invoice wasn't settled and bandwidth of htlc + // links hasn't been changed. + if invoice.Terms.Settled { + t.Fatal("alice invoice was settled") + } + + if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore { + t.Fatal("the bandwidth of alice channel link which handles " + + "alice->bob channel should be the same") + } + + if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "alice->bob channel should be the same") + } + + if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "bob->carol channel should be the same") + } + + if n.carolChannelLink.Bandwidth() != carolBandwidthBefore { + t.Fatal("the bandwidth of carol channel link which handles " + + "bob->carol channel should be the same") + } +} + +// TestChannelLinkMultiHopDecodeError checks that we send HTLC cancel if +// decoding of onion blob failed. +func TestChannelLinkMultiHopDecodeError(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + if err := n.start(); err != nil { + t.Fatalf("can't start three hop network: %v", err) + } + defer n.stop() + + // Replace decode function with another which throws an error. + n.carolChannelLink.cfg.DecodeOnion = func(r io.Reader, meta []byte) ( + HopIterator, error) { + return nil, errors.New("some sphinx decode error") + } + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + invoice, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + n.carolServer, + }, amount) + if err == nil { + t.Fatal("error haven't been received") + } else if err.Error() != errors.New(lnwire.SphinxParseError).Error() { + t.Fatalf("wrong error have been received: %v", err) + } + + // Wait for Bob to receive the revocation. + time.Sleep(100 * time.Millisecond) + + // Check that alice invoice wasn't settled and bandwidth of htlc + // links hasn't been changed. + if invoice.Terms.Settled { + t.Fatal("alice invoice was settled") + } + + if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore { + t.Fatal("the bandwidth of alice channel link which handles " + + "alice->bob channel should be the same") + } + + if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "alice->bob channel should be the same") + } + + if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore { + t.Fatal("the bandwidth of bob channel link which handles " + + "bob->carol channel should be the same") + } + + if n.carolChannelLink.Bandwidth() != carolBandwidthBefore { + t.Fatal("the bandwidth of carol channel link which handles " + + "bob->carol channel should be the same") + } +} + +// TestChannelLinkSingleHopMessageOrdering test checks ordering of message which +// flying around between Alice and Bob are correct when Bob sends payments to +// Alice. +func TestChannelLinkSingleHopMessageOrdering(t *testing.T) { + n := newThreeHopNetwork(t, + btcutil.SatoshiPerBitcoin*3, + btcutil.SatoshiPerBitcoin*5, + ) + + chanPoint := n.aliceChannelLink.ChanID() + + // Append initial channel window revocation messages which occurs after + // channel opening. + var aliceOrder []lnwire.Message + for i := 0; i < lnwallet.InitialRevocationWindow; i++ { + aliceOrder = append(aliceOrder, &lnwire.RevokeAndAck{}) + } + + // The order in which Alice receives wire messages. + aliceOrder = append(aliceOrder, []lnwire.Message{ + &lnwire.RevokeAndAck{}, + &lnwire.CommitSig{}, + &lnwire.UpdateFufillHTLC{}, + &lnwire.CommitSig{}, + &lnwire.RevokeAndAck{}, + }...) + + // Append initial channel window revocation messages which occurs after + // channel channel opening. + var bobOrder []lnwire.Message + for i := 0; i < lnwallet.InitialRevocationWindow; i++ { + bobOrder = append(bobOrder, &lnwire.RevokeAndAck{}) + } + + // The order in which Bob receives wire messages. + bobOrder = append(bobOrder, []lnwire.Message{ + &lnwire.UpdateAddHTLC{}, + &lnwire.CommitSig{}, + &lnwire.RevokeAndAck{}, + &lnwire.RevokeAndAck{}, + &lnwire.CommitSig{}, + }...) + + debug := false + if debug { + // Log message that alice receives. + n.aliceServer.record(createLogFunc("alice", + n.aliceChannelLink.ChanID())) + + // Log message that bob receives. + n.bobServer.record(createLogFunc("bob", + n.firstBobChannelLink.ChanID())) + } + + // Check that alice receives messages in right order. + n.aliceServer.record(func(m lnwire.Message) { + if getChanID(m) == chanPoint { + if len(aliceOrder) == 0 { + t.Fatal("redudant messages") + } + + if reflect.TypeOf(aliceOrder[0]) != reflect.TypeOf(m) { + t.Fatalf("alice received wrong message: \n"+ + "real: %v\n expected: %v", m.MsgType(), + aliceOrder[0].MsgType()) + } + aliceOrder = aliceOrder[1:] + } + }) + + // Check that bob receives messages in right order. + n.bobServer.record(func(m lnwire.Message) { + if getChanID(m) == chanPoint { + if len(bobOrder) == 0 { + t.Fatal("redudant messages") + } + + if reflect.TypeOf(bobOrder[0]) != reflect.TypeOf(m) { + t.Fatalf("bob received wrong message: \n"+ + "real: %v\n expected: %v", m.MsgType(), + bobOrder[0].MsgType()) + } + bobOrder = bobOrder[1:] + } + }) + + if err := n.start(); err != nil { + t.Fatalf("can't start three hop network: %v", err) + } + defer n.stop() + + // Wait for: + // * htlc add htlc request to be sent to alice + // * alice<->bob commitment state to be updated + // * settle request to be sent back from alice to bob + // * alice<->bob commitment state to be updated + var amount btcutil.Amount = btcutil.SatoshiPerBitcoin + if _, err := n.makePayment([]Peer{ + n.aliceServer, + n.bobServer, + }, amount); err != nil { + t.Fatalf("unable to make the payment: %v", err) + } +} diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index cf0d6428b..f51dff49d 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -2,15 +2,24 @@ package htlcswitch import ( "crypto/sha256" + "encoding/binary" "sync" "testing" + "io" "sync/atomic" + "github.com/btcsuite/fastsha256" "github.com/go-errors/errors" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/roasbeef/btcutil" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" ) type mockServer struct { @@ -28,6 +37,7 @@ type mockServer struct { id []byte htlcSwitch *Switch + registry *mockInvoiceRegistry recordFuncs []func(lnwire.Message) } @@ -35,12 +45,12 @@ var _ Peer = (*mockServer)(nil) func newMockServer(t *testing.T, name string) *mockServer { return &mockServer{ - t: t, - id: []byte(name), - name: name, - messages: make(chan lnwire.Message, 3000), - + t: t, + id: []byte(name), + name: name, + messages: make(chan lnwire.Message, 3000), quit: make(chan bool), + registry: newMockRegistry(), htlcSwitch: New(Config{}), recordFuncs: make([]func(lnwire.Message), 0), } @@ -78,6 +88,74 @@ func (s *mockServer) Start() error { return nil } +// mockHopIterator represents the test version of hop iterator which instead +// of encrypting the path in onion blob just stores the path as a list of hops. +type mockHopIterator struct { + hops []HopID +} + +func newMockHopIterator(hops ...HopID) HopIterator { + return &mockHopIterator{hops: hops} +} + +func (r *mockHopIterator) Next() *HopID { + if len(r.hops) != 0 { + next := r.hops[0] + r.hops = r.hops[1:] + return &next + } + + return nil +} + +func (r *mockHopIterator) Encode(w io.Writer) error { + var hopLength [4]byte + binary.BigEndian.PutUint32(hopLength[:], uint32(len(r.hops))) + + if _, err := w.Write(hopLength[:]); err != nil { + return err + } + + for _, hop := range r.hops { + if _, err := w.Write(hop[:]); err != nil { + return err + } + } + + return nil +} + +var _ HopIterator = (*mockHopIterator)(nil) + +// mockIteratorDecoder test version of hop iterator decoder which decodes the +// encoded array of hops. +type mockIteratorDecoder struct{} + +func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) ( + HopIterator, error) { + + var b [4]byte + _, err := r.Read(b[:]) + if err != nil { + return nil, err + } + hopLength := binary.BigEndian.Uint32(b[:]) + + hops := make([]HopID, hopLength) + for i := uint32(0); i < hopLength; i++ { + var hop HopID + + _, err := r.Read(hop[:]) + if err != nil { + return nil, err + } + + hops[i] = hop + } + + return newMockHopIterator(hops...), nil +} + // messageInterceptor is function that handles the incoming peer messages and // may decide should we handle it or not. type messageInterceptor func(m lnwire.Message) @@ -205,3 +283,105 @@ func (f *mockChannelLink) Start() error { return nil } func (f *mockChannelLink) Stop() {} var _ ChannelLink = (*mockChannelLink)(nil) + +type mockInvoiceRegistry struct { + sync.Mutex + invoices map[chainhash.Hash]*channeldb.Invoice +} + +func newMockRegistry() *mockInvoiceRegistry { + return &mockInvoiceRegistry{ + invoices: make(map[chainhash.Hash]*channeldb.Invoice), + } +} + +func (i *mockInvoiceRegistry) LookupInvoice(rHash chainhash.Hash) (*channeldb.Invoice, error) { + i.Lock() + defer i.Unlock() + + invoice, ok := i.invoices[rHash] + if !ok { + return nil, errors.New("can't find mock invoice") + } + + return invoice, nil +} + +func (i *mockInvoiceRegistry) SettleInvoice(rhash chainhash.Hash) error { + + invoice, err := i.LookupInvoice(rhash) + if err != nil { + return err + } + + i.Lock() + invoice.Terms.Settled = true + i.Unlock() + + return nil +} + +func (i *mockInvoiceRegistry) AddInvoice(invoice *channeldb.Invoice) error { + i.Lock() + defer i.Unlock() + + rhash := fastsha256.Sum256(invoice.Terms.PaymentPreimage[:]) + i.invoices[chainhash.Hash(rhash)] = invoice + return nil +} + +var _ InvoiceDatabase = (*mockInvoiceRegistry)(nil) + +type mockSigner struct { + key *btcec.PrivateKey +} + +func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) ([]byte, error) { + amt := signDesc.Output.Value + witnessScript := signDesc.WitnessScript + privKey := m.key + + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, + signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll, privKey) + if err != nil { + return nil, err + } + + return sig[:len(sig)-1], nil +} +func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) { + + witnessScript, err := txscript.WitnessScript(tx, signDesc.SigHashes, + signDesc.InputIndex, signDesc.Output.Value, signDesc.Output.PkScript, + txscript.SigHashAll, m.key, true) + if err != nil { + return nil, err + } + + return &lnwallet.InputScript{ + Witness: witnessScript, + }, nil +} + +type mockNotifier struct { +} + +func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, numConfs uint32) (*chainntnfs.ConfirmationEvent, error) { + return nil, nil +} +func (m *mockNotifier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) { + return nil, nil +} + +func (m *mockNotifier) Start() error { + return nil +} + +func (m *mockNotifier) Stop() error { + return nil +} +func (m *mockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.SpendEvent, error) { + return &chainntnfs.SpendEvent{ + Spend: make(chan *chainntnfs.SpendDetail), + }, nil +} diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go new file mode 100644 index 000000000..b3b5e4cb9 --- /dev/null +++ b/htlcswitch/test_utils.go @@ -0,0 +1,507 @@ +package htlcswitch + +import ( + "bytes" + "crypto/sha256" + "math/rand" + "testing" + "time" + + "io/ioutil" + "os" + + "github.com/btcsuite/fastsha256" + "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +var ( + alicePrivKey = []byte("alice priv key") + bobPrivKey = []byte("bob priv key") + carolPrivKey = []byte("carol priv key") +) + +// generateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func generateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + + _, err := rand.Read(b[:]) + // Note that Err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} + +// createTestChannel creates the channel and returns our and remote channels +// representations. +func createTestChannel(alicePrivKey, bobPrivKey []byte, + aliceAmount, bobAmount btcutil.Amount) ( + *lnwallet.LightningChannel, *lnwallet.LightningChannel, func(), error) { + + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), alicePrivKey) + bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), bobPrivKey) + + channelCapacity := aliceAmount + bobAmount + aliceDustLimit := btcutil.Amount(200) + bobDustLimit := btcutil.Amount(800) + csvTimeoutAlice := uint32(5) + csvTimeoutBob := uint32(4) + + witnessScript, _, err := lnwallet.GenFundingPkScript( + aliceKeyPub.SerializeCompressed(), + bobKeyPub.SerializeCompressed(), + int64(channelCapacity), + ) + if err != nil { + return nil, nil, nil, err + } + + var hash [sha256.Size]byte + randomSeed, err := generateRandomBytes(sha256.Size) + if err != nil { + return nil, nil, nil, err + } + copy(hash[:], randomSeed) + + prevOut := &wire.OutPoint{ + Hash: chainhash.Hash(hash), + Index: 0, + } + fundingTxIn := wire.NewTxIn(prevOut, nil, nil) + + bobRoot := lnwallet.DeriveRevocationRoot(bobKeyPriv, bobKeyPub, + aliceKeyPub) + bobPreimageProducer := shachain.NewRevocationProducer(*bobRoot) + bobFirstRevoke, err := bobPreimageProducer.AtIndex(0) + if err != nil { + return nil, nil, nil, err + } + bobRevokeKey := lnwallet.DeriveRevocationPubkey(aliceKeyPub, + bobFirstRevoke[:]) + + aliceRoot := lnwallet.DeriveRevocationRoot(aliceKeyPriv, aliceKeyPub, + bobKeyPub) + alicePreimageProducer := shachain.NewRevocationProducer(*aliceRoot) + aliceFirstRevoke, err := alicePreimageProducer.AtIndex(0) + if err != nil { + return nil, nil, nil, err + } + aliceRevokeKey := lnwallet.DeriveRevocationPubkey(bobKeyPub, + aliceFirstRevoke[:]) + + aliceCommitTx, err := lnwallet.CreateCommitTx( + fundingTxIn, + aliceKeyPub, + bobKeyPub, + aliceRevokeKey, + csvTimeoutAlice, + aliceAmount, + bobAmount, + lnwallet.DefaultDustLimit(), + ) + if err != nil { + return nil, nil, nil, err + } + bobCommitTx, err := lnwallet.CreateCommitTx( + fundingTxIn, + bobKeyPub, + aliceKeyPub, + bobRevokeKey, + csvTimeoutBob, + bobAmount, + aliceAmount, + lnwallet.DefaultDustLimit(), + ) + if err != nil { + return nil, nil, nil, err + } + + alicePath, err := ioutil.TempDir("", "alicedb") + dbAlice, err := channeldb.Open(alicePath) + if err != nil { + return nil, nil, nil, err + } + + bobPath, err := ioutil.TempDir("", "bobdb") + dbBob, err := channeldb.Open(bobPath) + if err != nil { + return nil, nil, nil, err + } + + var obsfucator [lnwallet.StateHintSize]byte + copy(obsfucator[:], aliceFirstRevoke[:]) + + aliceChannelState := &channeldb.OpenChannel{ + IdentityPub: aliceKeyPub, + ChanID: prevOut, + ChanType: channeldb.SingleFunder, + IsInitiator: true, + StateHintObsfucator: obsfucator, + OurCommitKey: aliceKeyPub, + TheirCommitKey: bobKeyPub, + Capacity: channelCapacity, + OurBalance: aliceAmount, + TheirBalance: bobAmount, + OurCommitTx: aliceCommitTx, + OurCommitSig: bytes.Repeat([]byte{1}, 71), + FundingOutpoint: prevOut, + OurMultiSigKey: aliceKeyPub, + TheirMultiSigKey: bobKeyPub, + FundingWitnessScript: witnessScript, + LocalCsvDelay: csvTimeoutAlice, + RemoteCsvDelay: csvTimeoutBob, + TheirCurrentRevocation: bobRevokeKey, + RevocationProducer: alicePreimageProducer, + RevocationStore: shachain.NewRevocationStore(), + TheirDustLimit: bobDustLimit, + OurDustLimit: aliceDustLimit, + Db: dbAlice, + } + bobChannelState := &channeldb.OpenChannel{ + IdentityPub: bobKeyPub, + ChanID: prevOut, + ChanType: channeldb.SingleFunder, + IsInitiator: false, + StateHintObsfucator: obsfucator, + OurCommitKey: bobKeyPub, + TheirCommitKey: aliceKeyPub, + Capacity: channelCapacity, + OurBalance: bobAmount, + TheirBalance: aliceAmount, + OurCommitTx: bobCommitTx, + OurCommitSig: bytes.Repeat([]byte{1}, 71), + FundingOutpoint: prevOut, + OurMultiSigKey: bobKeyPub, + TheirMultiSigKey: aliceKeyPub, + FundingWitnessScript: witnessScript, + LocalCsvDelay: csvTimeoutBob, + RemoteCsvDelay: csvTimeoutAlice, + TheirCurrentRevocation: aliceRevokeKey, + RevocationProducer: bobPreimageProducer, + RevocationStore: shachain.NewRevocationStore(), + TheirDustLimit: aliceDustLimit, + OurDustLimit: bobDustLimit, + Db: dbBob, + } + + cleanUpFunc := func() { + os.RemoveAll(bobPath) + os.RemoveAll(alicePath) + } + + aliceSigner := &mockSigner{aliceKeyPriv} + bobSigner := &mockSigner{bobKeyPriv} + estimator := &lnwallet.StaticFeeEstimator{ + FeeRate: 24, + Confirmation: 6, + } + + channelAlice, err := lnwallet.NewLightningChannel(aliceSigner, + nil, estimator, aliceChannelState) + if err != nil { + return nil, nil, nil, err + } + channelBob, err := lnwallet.NewLightningChannel(bobSigner, nil, + estimator, bobChannelState) + if err != nil { + return nil, nil, nil, err + } + + return channelAlice, channelBob, cleanUpFunc, nil +} + +// getChanID retrieves the channel point from nwire message. +func getChanID(msg lnwire.Message) lnwire.ChannelID { + var point lnwire.ChannelID + switch msg := msg.(type) { + case *lnwire.UpdateAddHTLC: + point = msg.ChanID + case *lnwire.UpdateFufillHTLC: + point = msg.ChanID + case *lnwire.UpdateFailHTLC: + point = msg.ChanID + case *lnwire.RevokeAndAck: + point = msg.ChanID + case *lnwire.CommitSig: + point = msg.ChanID + } + + return point +} + +// generatePayment generates the htlc add request by given path blob and +// invoice which should be added by destination peer. +func generatePayment(amount btcutil.Amount, blob [lnwire.OnionPacketSize]byte) ( + *channeldb.Invoice, *lnwire.UpdateAddHTLC, error) { + + // Initialize random seed with unix time in order to generate random + // preimage every time. + rand.Seed(time.Now().UTC().UnixNano()) + + var preimage [sha256.Size]byte + r, err := generateRandomBytes(sha256.Size) + if err != nil { + return nil, nil, err + } + copy(preimage[:], r) + rhash := fastsha256.Sum256(preimage[:]) + + // Generate and add the invoice in carol invoice registry as far as + // htlc request should go to the + return &channeldb.Invoice{ + CreationDate: time.Now(), + Terms: channeldb.ContractTerm{ + Value: amount, + PaymentPreimage: preimage, + }, + }, + &lnwire.UpdateAddHTLC{ + PaymentHash: rhash, + Amount: amount, + OnionBlob: blob, + }, nil +} + +// generateRoute generates the path blob by given array of peers. +func generateRoute(peers []Peer) ([]byte, [lnwire.OnionPacketSize]byte, error) { + var blob [lnwire.OnionPacketSize]byte + if len(peers) == 0 { + return nil, blob, errors.New("empty path") + } + + // Create array of hops in order to create onion blob. + hops := make([]HopID, len(peers)-1) + for i, peer := range peers[1:] { + hops[i] = NewHopID(peer.PubKey()) + } + + // Initialize iterator and encode it. + var b bytes.Buffer + iterator := newMockHopIterator(hops...) + if err := iterator.Encode(&b); err != nil { + return nil, blob, err + } + copy(blob[:], b.Bytes()) + + return peers[0].PubKey(), blob, nil + +} + +// threeHopNetwork is used for managing the created cluster of 3 hops. +type threeHopNetwork struct { + aliceServer *mockServer + aliceChannelLink *channelLink + + firstBobChannelLink *channelLink + bobServer *mockServer + secondBobChannelLink *channelLink + + carolChannelLink *channelLink + carolServer *mockServer + + firstChannelCleanup func() + secondChannelCleanup func() +} + +// makePayment takes the destination node and amount as input, sends the +// payment and returns the error channel to wait for error to be received and +// invoice in order to check its status after the payment finished. +// +// With this function you can send payments: +// * from Alice to Bob +// * from Alice to Carol through the Bob +// * from Alice to some another peer through the Bob +func (n *threeHopNetwork) makePayment(peers []Peer, + amount btcutil.Amount) (*channeldb.Invoice, error) { + + // Extract sender peer. + senderPeer := peers[0].(*mockServer) + peers = peers[1:] + + // Generate route convert it to blob, and return next destination for + // htlc add request. + firstNode, blob, err := generateRoute(peers) + if err != nil { + return nil, err + } + + // Generate payment: invoice and htlc. + invoice, htlc, err := generatePayment(amount, blob) + if err != nil { + return nil, err + } + + // Check who is last in the route and add invoice to server registry. + receiverPeer := peers[len(peers)-1].(*mockServer) + if err := receiverPeer.registry.AddInvoice(invoice); err != nil { + return nil, err + } + + // Send payment and expose err channel. + errChan := make(chan error) + go func() { + _, err := senderPeer.htlcSwitch.SendHTLC(firstNode, htlc) + errChan <- err + }() + + select { + case err := <-errChan: + return invoice, err + case <-time.After(6 * time.Second): + return invoice, errors.New("htlc was no settled in time") + } +} + +// start starts the three hop network alice,bob,carol servers. +func (n *threeHopNetwork) start() error { + if err := n.aliceServer.Start(); err != nil { + return err + } + if err := n.bobServer.Start(); err != nil { + return err + } + if err := n.carolServer.Start(); err != nil { + return err + } + + return nil +} + +// stop stops nodes and cleanup its databases. +func (n *threeHopNetwork) stop() { + done := make(chan struct{}) + go func() { + n.aliceServer.Stop() + done <- struct{}{} + }() + + go func() { + n.bobServer.Stop() + done <- struct{}{} + }() + + go func() { + n.carolServer.Stop() + done <- struct{}{} + }() + + for i := 0; i < 3; i++ { + <-done + } + + n.firstChannelCleanup() + n.secondChannelCleanup() +} + +// newThreeHopNetwork function creates the following topology and returns the +// control object to manage this cluster: +// +// alice bob carol +// server - <-connection-> - server - - <-connection-> - - - server +// | | | +// alice htlc bob htlc carol htlc +// switch switch \ switch +// | | \ | +// | | \ | +// alice first bob second bob carol +// channel link channel link channel link channel link +// +func newThreeHopNetwork(t *testing.T, aliceToBob, + bobToCarol btcutil.Amount) *threeHopNetwork { + var err error + + // Create three peers/servers. + aliceServer := newMockServer(t, "alice") + bobServer := newMockServer(t, "bob") + carolServer := newMockServer(t, "carol") + + // Create mock decoder instead of sphinx one in order to mock the + // route which htlc should follow. + decoder := &mockIteratorDecoder{} + + // Create lightning channels between Alice<->Bob and Bob<->Carol + aliceChannel, firstBobChannel, fCleanUp, err := createTestChannel( + alicePrivKey, bobPrivKey, aliceToBob, aliceToBob) + if err != nil { + t.Fatalf("unable to create alice<->bob channel: %v", err) + } + + secondBobChannel, carolChannel, sCleanUp, err := createTestChannel( + bobPrivKey, carolPrivKey, bobToCarol, bobToCarol) + if err != nil { + t.Fatalf("unable to create bob<->carol channel: %v", err) + } + + aliceChannelLink := NewChannelLink( + &ChannelLinkConfig{ + // htlc responses will be sent to this node + Peer: bobServer, + // htlc will be propagated to this switch + Switch: aliceServer.htlcSwitch, + // route will be generated by this decoder + DecodeOnion: decoder.Decode, + Registry: aliceServer.registry, + }, aliceChannel) + if err := aliceServer.htlcSwitch.addLink(aliceChannelLink); err != nil { + t.Fatalf("unable to add alice channel link: %v", err) + } + + firstBobChannelLink := NewChannelLink( + &ChannelLinkConfig{ + Peer: aliceServer, + Switch: bobServer.htlcSwitch, + DecodeOnion: decoder.Decode, + Registry: bobServer.registry, + }, firstBobChannel) + if err := bobServer.htlcSwitch.addLink(firstBobChannelLink); err != nil { + t.Fatalf("unable to add first bob channel link: %v", err) + } + + secondBobChannelLink := NewChannelLink( + &ChannelLinkConfig{ + Peer: carolServer, + Switch: bobServer.htlcSwitch, + DecodeOnion: decoder.Decode, + Registry: bobServer.registry, + }, secondBobChannel) + + if err := bobServer.htlcSwitch.addLink(secondBobChannelLink); err != nil { + t.Fatalf("unable to add second bob channel link: %v", err) + } + + carolChannelLink := NewChannelLink( + &ChannelLinkConfig{ + Peer: bobServer, + Switch: carolServer.htlcSwitch, + DecodeOnion: decoder.Decode, + Registry: carolServer.registry, + }, carolChannel) + if err := carolServer.htlcSwitch.addLink(carolChannelLink); err != nil { + t.Fatalf("unable to add carol channel link: %v", err) + } + + return &threeHopNetwork{ + aliceServer: aliceServer, + aliceChannelLink: aliceChannelLink.(*channelLink), + firstBobChannelLink: firstBobChannelLink.(*channelLink), + bobServer: bobServer, + secondBobChannelLink: secondBobChannelLink.(*channelLink), + carolChannelLink: carolChannelLink.(*channelLink), + carolServer: carolServer, + + firstChannelCleanup: fCleanUp, + secondChannelCleanup: sCleanUp, + } +}