diff --git a/lnd_test.go b/lnd_test.go new file mode 100644 index 00000000..94d98a1a --- /dev/null +++ b/lnd_test.go @@ -0,0 +1,187 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "runtime/debug" + "strings" + "testing" + "time" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/roasbeef/btcd/rpctest" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +type lndTestCase func(net *networkHarness, t *testing.T) + +func assertTxInBlock(block *btcutil.Block, txid *wire.ShaHash, t *testing.T) { + for _, tx := range block.Transactions() { + if bytes.Equal(txid[:], tx.Sha()[:]) { + return + } + } + + t.Fatalf("funding tx was not included in block") +} + +// testBasicChannelFunding performs a test excercising expected behavior from a +// basic funding workflow. The test creates a new channel between Alice and +// Bob, then immediately closes the channel after asserting some expected post +// conditions. Finally, the chain itelf is checked to ensure the closing +// transaction was mined. +func testBasicChannelFunding(net *networkHarness, t *testing.T) { + ctxb := context.Background() + + // First establish a channel between Alice and Bob. + openReq := &lnrpc.OpenChannelRequest{ + // TODO(roasbeef): should pass actual id instead, will fail if + // more connections added for Alice. + TargetPeerId: 1, + LocalFundingAmount: btcutil.SatoshiPerBitcoin / 2, + RemoteFundingAmount: 0, + NumConfs: 1, + } + time.Sleep(time.Millisecond * 500) + respStream, err := net.AliceClient.OpenChannel(ctxb, openReq) + if err != nil { + t.Fatalf("unable to open channel between alice and bob: %v", err) + } + + // Mine a block, the funding txid should be included, and both nodes should + // be aware of the channel. + // TODO(roasbeef): replace sleep with something more robust + time.Sleep(time.Second * 1) + blockHash, err := net.Miner.Node.Generate(1) + if err != nil { + t.Fatalf("unable to generate block: %v", err) + } + resp, err := respStream.Recv() + if err != nil { + t.Fatalf("unable to read rpc resp: %v", err) + } + block, err := net.Miner.Node.GetBlock(blockHash[0]) + if err != nil { + t.Fatalf("unable to get block: %v", err) + } + if len(block.Transactions()) < 2 { + t.Fatalf("funding transaction not included") + } + + fundingTxID, _ := wire.NewShaHash(resp.ChannelPoint.FundingTxid) + fundingTxIDStr := fundingTxID.String() + assertTxInBlock(block, fundingTxID, t) + + // TODO(roasbeef): remove and use "listchannels" command after + // implemented. + req := &lnrpc.ListPeersRequest{} + alicePeerInfo, err := net.AliceClient.ListPeers(ctxb, req) + if err != nil { + t.Fatalf("unable to list alice peers: %v", err) + } + bobPeerInfo, err := net.BobClient.ListPeers(ctxb, req) + if err != nil { + t.Fatalf("unable to list bob peers: %v", err) + } + + // The channel should be listed in the peer information returned by + // both peers. + aliceTxID := alicePeerInfo.Peers[0].Channels[0].ChannelPoint + bobTxID := bobPeerInfo.Peers[0].Channels[0].ChannelPoint + if !strings.Contains(bobTxID, fundingTxIDStr) { + t.Fatalf("alice's channel not found") + } + if !strings.Contains(aliceTxID, fundingTxIDStr) { + t.Fatalf("bob's channel not found") + } + + // Initiate a close from Alice's side. After mining a block, the closing + // transaction should be included, and both nodes should have forgotten + // about the channel. + closeReq := &lnrpc.CloseChannelRequest{ + ChannelPoint: resp.ChannelPoint, + } + closeRespStream, err := net.AliceClient.CloseChannel(ctxb, closeReq) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + time.Sleep(time.Second * 1) + blockHash, err = net.Miner.Node.Generate(1) + if err != nil { + t.Fatalf("unable to generate block: %v", err) + } + block, err = net.Miner.Node.GetBlock(blockHash[0]) + if err != nil { + t.Fatalf("unable to get block: %v", err) + } + closeResp, err := closeRespStream.Recv() + if err != nil { + t.Fatalf("unable to read rpc resp: %v", err) + } + + closingTxID, _ := wire.NewShaHash(closeResp.ClosingTxid) + assertTxInBlock(block, closingTxID, t) +} + +var lndTestCases = map[string]lndTestCase{ + "basic funding flow": testBasicChannelFunding, +} + +// TestLightningNetworkDaemon performs a series of integration tests amongst a +// programatically driven network of lnd nodes. +func TestLightningNetworkDaemon(t *testing.T) { + var btcdHarness *rpctest.Harness + var lightningNetwork *networkHarness + var currentTest string + var err error + + defer func() { + // If one of the integration tests caused a panic within the main + // goroutine, then tear down all the harnesses in order to avoid + // any leaked processes. + if r := recover(); r != nil { + fmt.Println("recovering from test panic: ", r) + if err := btcdHarness.TearDown(); err != nil { + fmt.Println("unable to tear btcd harnesses: ", err) + } + if err := lightningNetwork.TearDownAll(); err != nil { + fmt.Println("unable to tear lnd harnesses: ", err) + } + t.Fatalf("test %v panicked: %s", currentTest, debug.Stack()) + } + }() + + // First create an intance of the btcd's rpctest.Harness. This will be + // used to fund the wallets of the nodes within the test network and to + // drive blockchain related events within the network. + btcdHarness, err = rpctest.New(harnessNetParams, nil, nil) + if err != nil { + t.Fatalf("unable to create mining node: %v", err) + } + defer btcdHarness.TearDown() + if err = btcdHarness.SetUp(true, 50); err != nil { + t.Fatalf("unable to set up mining node: %v", err) + } + + // With the btcd harness created, create an instance of the lightning + // network harness as it depends on the btcd harness to script network + // activity. + lightningNetwork, err = newNetworkHarness(btcdHarness, nil) + if err != nil { + t.Fatalf("unable to create lightning network harness: %v", err) + } + defer lightningNetwork.TearDownAll() + if err = lightningNetwork.SetUp(); err != nil { + t.Fatalf("unable to set up test lightning network: %v", err) + } + + t.Logf("Running %v integration tests", len(lndTestCases)) + for testName, lnTest := range lndTestCases { + t.Logf("Executing test %v", testName) + + currentTest = testName + lnTest(lightningNetwork, t) + } +} diff --git a/networkharness.go b/networkharness.go index 9ec41fb4..758d3d73 100644 --- a/networkharness.go +++ b/networkharness.go @@ -230,6 +230,9 @@ type networkHarness struct { // running instance of btcd's rpctest harness. Any extra command line flags // which should be passed to create lnd instance should be formatted properly // in the lndArgs slice (--arg=value). +// TODO(roasbeef): add option to use golang's build library to a binary of the +// current repo. This'll save developers from having to manually `go install` +// within the repo each time before changes. func newNetworkHarness(r *rpctest.Harness, lndArgs []string) (*networkHarness, error) { var err error