diff --git a/.gitignore b/.gitignore index 290cf568..796fd68c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ _testmain.go # Integration test log files output*.log /.backendlogs +/.minerlogs cmd/cmd *.key diff --git a/lnd_test.go b/lnd_test.go index cb78e648..be94669d 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -21,6 +21,7 @@ import ( "crypto/rand" "crypto/sha256" + "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" @@ -1542,57 +1543,110 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint3, false) } +// waitForNodeBlockHeight queries the node for its current block height until +// it reaches the passed height. +func waitForNodeBlockHeight(ctx context.Context, node *lntest.HarnessNode, + height int32) error { + var predErr error + err := lntest.WaitPredicate(func() bool { + ctxt, _ := context.WithTimeout(ctx, 10*time.Second) + info, err := node.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) + if err != nil { + predErr = err + return false + } + + if int32(info.BlockHeight) != height { + predErr = fmt.Errorf("expected block height to "+ + "be %v, was %v", height, info.BlockHeight) + return false + } + return true + }, 15*time.Second) + if err != nil { + return predErr + } + return nil +} + +// assertMinerBlockHeightDelta ensures that tempMiner is 'delta' blocks ahead +// of miner. +func assertMinerBlockHeightDelta(t *harnessTest, + miner, tempMiner *rpctest.Harness, delta int32) { + + // Ensure the chain lengths are what we expect. + var predErr error + err := lntest.WaitPredicate(func() bool { + _, tempMinerHeight, err := tempMiner.Node.GetBestBlock() + if err != nil { + predErr = fmt.Errorf("unable to get current "+ + "blockheight %v", err) + return false + } + + _, minerHeight, err := miner.Node.GetBestBlock() + if err != nil { + predErr = fmt.Errorf("unable to get current "+ + "blockheight %v", err) + return false + } + + if tempMinerHeight != minerHeight+delta { + predErr = fmt.Errorf("expected new miner(%d) to be %d "+ + "blocks ahead of original miner(%d)", + tempMinerHeight, delta, minerHeight) + return false + } + return true + }, time.Second*15) + if err != nil { + t.Fatalf(predErr.Error()) + } +} + // testOpenChannelAfterReorg tests that in the case where we have an open // channel where the funding tx gets reorged out, the channel will no // longer be present in the node's routing table. func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() + var ( + ctxb = context.Background() + temp = "temp" + perm = "perm" + ) // Set up a new miner that we can use to cause a reorg. args := []string{"--rejectnonstd", "--txindex"} - miner, err := rpctest.New(harnessNetParams, + tempMiner, err := rpctest.New(harnessNetParams, &rpcclient.NotificationHandlers{}, args) if err != nil { t.Fatalf("unable to create mining node: %v", err) } - if err := miner.SetUp(true, 50); err != nil { + if err := tempMiner.SetUp(false, 0); err != nil { t.Fatalf("unable to set up mining node: %v", err) } - defer miner.TearDown() - - if err := miner.Node.NotifyNewTransactions(false); err != nil { - t.Fatalf("unable to request transaction notifications: %v", err) - } + defer tempMiner.TearDown() // We start by connecting the new miner to our original miner, // such that it will sync to our original chain. - if err := rpctest.ConnectNode(net.Miner, miner); err != nil { - t.Fatalf("unable to connect harnesses: %v", err) + err = net.Miner.Node.Node( + btcjson.NConnect, tempMiner.P2PAddress(), &temp, + ) + if err != nil { + t.Fatalf("unable to remove node: %v", err) } - nodeSlice := []*rpctest.Harness{net.Miner, miner} + nodeSlice := []*rpctest.Harness{net.Miner, tempMiner} if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil { t.Fatalf("unable to join node on blocks: %v", err) } - // The two should be on the same blockheight. - _, newNodeHeight, err := miner.Node.GetBestBlock() - if err != nil { - t.Fatalf("unable to get current blockheight %v", err) - } + // The two miners should be on the same blockheight. + assertMinerBlockHeightDelta(t, net.Miner, tempMiner, 0) - _, orgNodeHeight, err := net.Miner.Node.GetBestBlock() - if err != nil { - t.Fatalf("unable to get current blockheight %v", err) - } - - if newNodeHeight != orgNodeHeight { - t.Fatalf("expected new miner(%d) and original miner(%d) to "+ - "be on the same height", newNodeHeight, orgNodeHeight) - } - - // We disconnect the two nodes, such that we can start mining on them - // individually without the other one learning about the new blocks. - err = net.Miner.Node.AddNode(miner.P2PAddress(), rpcclient.ANRemove) + // We disconnect the two miners, such that we can mine two different + // chains and can cause a reorg later. + err = net.Miner.Node.Node( + btcjson.NDisconnect, tempMiner.P2PAddress(), &temp, + ) if err != nil { t.Fatalf("unable to remove node: %v", err) } @@ -1608,7 +1662,8 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("unable to open channel: %v", err) } - // Wait for miner to have seen the funding tx. + // Wait for miner to have seen the funding tx. The temporary miner is + // disconnected, and won't see the transaction. _, err = waitForTxInMempool(net.Miner.Node, minerMempoolTimeout) if err != nil { t.Fatalf("failed to find funding tx in mempool: %v", err) @@ -1627,25 +1682,27 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { // We now cause a fork, by letting our original miner mine 10 blocks, // and our new miner mine 15. This will also confirm our pending - // channel, which should be considered open. + // channel on the original miner's chain, which should be considered + // open. block := mineBlocks(t, net, 10, 1)[0] assertTxInBlock(t, block, fundingTxID) - miner.Node.Generate(15) + if _, err := tempMiner.Node.Generate(15); err != nil { + t.Fatalf("unable to generate blocks: %v", err) + } - // Ensure the chain lengths are what we expect. - _, newNodeHeight, err = miner.Node.GetBestBlock() + // Ensure the chain lengths are what we expect, with the temp miner + // being 5 blocks ahead. + assertMinerBlockHeightDelta(t, net.Miner, tempMiner, 5) + + // Wait for Alice to sync to the original miner's chain. + _, minerHeight, err := net.Miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current blockheight %v", err) } - - _, orgNodeHeight, err = net.Miner.Node.GetBestBlock() + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = waitForNodeBlockHeight(ctxt, net.Alice, minerHeight) if err != nil { - t.Fatalf("unable to get current blockheight %v", err) - } - - if newNodeHeight != orgNodeHeight+5 { - t.Fatalf("expected new miner(%d) to be 5 blocks ahead of "+ - "original miner(%d)", newNodeHeight, orgNodeHeight) + t.Fatalf("unable to sync to chain: %v", err) } chanPoint := &lnrpc.ChannelPoint{ @@ -1689,49 +1746,86 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { numEdges) } - // Connecting the two miners should now cause our original one to sync - // to the new, and longer chain. - if err := rpctest.ConnectNode(net.Miner, miner); err != nil { - t.Fatalf("unable to connect harnesses: %v", err) + // Now we disconnect Alice's chain backend from the original miner, and + // connect the two miners together. Since the temporary miner knows + // about a longer chain, both miners should sync to that chain. + err = net.Miner.Node.Node( + btcjson.NRemove, net.BackendCfg.P2PAddr(), &perm, + ) + if err != nil { + t.Fatalf("unable to remove node: %v", err) } - if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil { + // Connecting to the temporary miner should now cause our original + // chain to be re-orged out. + err = net.Miner.Node.Node( + btcjson.NConnect, tempMiner.P2PAddress(), &temp, + ) + if err != nil { + t.Fatalf("unable to remove node: %v", err) + } + + nodes := []*rpctest.Harness{tempMiner, net.Miner} + if err := rpctest.JoinNodes(nodes, rpctest.Blocks); err != nil { t.Fatalf("unable to join node on blocks: %v", err) } // Once again they should be on the same chain. - _, newNodeHeight, err = miner.Node.GetBestBlock() + assertMinerBlockHeightDelta(t, net.Miner, tempMiner, 0) + + // Now we disconnect the two miners, and connect our original miner to + // our chain backend once again. + err = net.Miner.Node.Node( + btcjson.NDisconnect, tempMiner.P2PAddress(), &temp, + ) + if err != nil { + t.Fatalf("unable to remove node: %v", err) + } + + err = net.Miner.Node.Node( + btcjson.NConnect, net.BackendCfg.P2PAddr(), &perm, + ) + if err != nil { + t.Fatalf("unable to remove node: %v", err) + } + + // This should have caused a reorg, and Alice should sync to the longer + // chain, where the funding transaction is not confirmed. + _, tempMinerHeight, err := tempMiner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current blockheight %v", err) } - - _, orgNodeHeight, err = net.Miner.Node.GetBestBlock() + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = waitForNodeBlockHeight(ctxt, net.Alice, tempMinerHeight) if err != nil { - t.Fatalf("unable to get current blockheight %v", err) + t.Fatalf("unable to sync to chain: %v", err) } - if newNodeHeight != orgNodeHeight { - t.Fatalf("expected new miner(%d) and original miner(%d) to "+ - "be on the same height", newNodeHeight, orgNodeHeight) - } - - time.Sleep(time.Second * 2) - // Since the fundingtx was reorged out, Alice should now have no edges // in her graph. req = &lnrpc.ChannelGraphRequest{ IncludeUnannounced: true, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - chanGraph, err = net.Alice.DescribeGraph(ctxt, req) - if err != nil { - t.Fatalf("unable to query for alice's routing table: %v", err) - } - numEdges = len(chanGraph.Edges) - if numEdges != 0 { - t.Fatalf("expected to find no edge in the graph, found %d", - numEdges) + var predErr error + err = lntest.WaitPredicate(func() bool { + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + chanGraph, err = net.Alice.DescribeGraph(ctxt, req) + if err != nil { + predErr = fmt.Errorf("unable to query for alice's routing table: %v", err) + return false + } + + numEdges = len(chanGraph.Edges) + if numEdges != 0 { + predErr = fmt.Errorf("expected to find no edge in the graph, found %d", + numEdges) + return false + } + return true + }, time.Second*15) + if err != nil { + t.Fatalf(predErr.Error()) } // Cleanup by mining the funding tx again, then closing the channel. @@ -12930,49 +13024,73 @@ var testsCases = []*testCase{ func TestLightningNetworkDaemon(t *testing.T) { ht := newHarnessTest(t) + // Start a btcd chain backend. + chainBackend, cleanUp, err := lntest.NewBtcdBackend() + if err != nil { + ht.Fatalf("unable to start btcd: %v", err) + } + defer cleanUp() + + // Declare the network harness here to gain access to its + // 'OnTxAccepted' call back. var lndHarness *lntest.NetworkHarness - // First create an instance 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. Revert the default - // setting of accepting non-standard transactions on simnet to reject them. - // Transactions on the lightning network should always be standard to get - // better guarantees of getting included in to blocks. - logDir := "./.backendlogs" + // Create an instance of the btcd's rpctest.Harness that will act as + // the miner for all tests. This will be used to fund the wallets of + // the nodes within the test network and to drive blockchain related + // events within the network. Revert the default setting of accepting + // non-standard transactions on simnet to reject them. Transactions on + // the lightning network should always be standard to get better + // guarantees of getting included in to blocks. + // + // We will also connect it to our chain backend. + minerLogDir := "./.minerlogs" args := []string{ "--rejectnonstd", "--txindex", "--debuglevel=debug", - "--logdir=" + logDir, + "--logdir=" + minerLogDir, + "--trickleinterval=100ms", + "--connect=" + chainBackend.P2PAddr(), } handlers := &rpcclient.NotificationHandlers{ OnTxAccepted: func(hash *chainhash.Hash, amt btcutil.Amount) { lndHarness.OnTxAccepted(hash) }, } - btcdHarness, err := rpctest.New(harnessNetParams, handlers, args) + + miner, err := rpctest.New(harnessNetParams, handlers, args) if err != nil { ht.Fatalf("unable to create mining node: %v", err) } defer func() { - btcdHarness.TearDown() + miner.TearDown() - // After shutting down the chain backend, we'll make a copy of - // the log file before deleting the temporary log dir. - logFile := logDir + "/" + harnessNetParams.Name + "/btcd.log" - err := lntest.CopyFile("./output_btcd_chainbackend.log", - logFile) + // After shutting down the miner, we'll make a copy of the log + // file before deleting the temporary log dir. + logFile := fmt.Sprintf( + "%s/%s/btcd.log", minerLogDir, harnessNetParams.Name, + ) + err := lntest.CopyFile("./output_btcd_miner.log", logFile) if err != nil { fmt.Printf("unable to copy file: %v\n", err) } - if err = os.RemoveAll(logDir); err != nil { - fmt.Printf("Cannot remove dir %s: %v\n", logDir, err) + if err = os.RemoveAll(minerLogDir); err != nil { + fmt.Printf("Cannot remove dir %s: %v\n", + minerLogDir, err) } }() - // First create the network harness to gain access to its - // 'OnTxAccepted' call back. - lndHarness, err = lntest.NewNetworkHarness(btcdHarness) + if err := miner.SetUp(true, 50); err != nil { + ht.Fatalf("unable to set up mining node: %v", err) + } + if err := miner.Node.NotifyNewTransactions(false); err != nil { + ht.Fatalf("unable to request transaction notifications: %v", err) + } + + // Now we can set up our test harness (LND instance), with the chain + // backend we just created. + lndHarness, err = lntest.NewNetworkHarness(miner, chainBackend) if err != nil { ht.Fatalf("unable to create lightning network harness: %v", err) } @@ -12994,17 +13112,10 @@ func TestLightningNetworkDaemon(t *testing.T) { } }() - if err := btcdHarness.SetUp(true, 50); err != nil { - ht.Fatalf("unable to set up mining node: %v", err) - } - if err := btcdHarness.Node.NotifyNewTransactions(false); err != nil { - ht.Fatalf("unable to request transaction notifications: %v", err) - } - // Next mine enough blocks in order for segwit and the CSV package // soft-fork to activate on SimNet. numBlocks := chaincfg.SimNetParams.MinerConfirmationWindow * 2 - if _, err := btcdHarness.Node.Generate(numBlocks); err != nil { + if _, err := miner.Node.Generate(numBlocks); err != nil { ht.Fatalf("unable to generate blocks: %v", err) } diff --git a/lntest/btcd.go b/lntest/btcd.go new file mode 100644 index 00000000..48e4d71f --- /dev/null +++ b/lntest/btcd.go @@ -0,0 +1,87 @@ +package lntest + +import ( + "encoding/hex" + "fmt" + "os" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" +) + +// logDir is the name of the temporary log directory. +const logDir = "./.backendlogs" + +// BtcdBackendConfig is an implementation of the BackendConfig interface +// backed by a btcd node. +type BtcdBackendConfig struct { + // rpcConfig houses the connection config to the backing btcd instance. + rpcConfig rpcclient.ConnConfig + + // p2pAddress is the p2p address of the btcd instance. + p2pAddress string +} + +// GenArgs returns the arguments needed to be passed to LND at startup for +// using this node as a chain backend. +func (b BtcdBackendConfig) GenArgs() []string { + var args []string + encodedCert := hex.EncodeToString(b.rpcConfig.Certificates) + args = append(args, "--bitcoin.node=btcd") + args = append(args, fmt.Sprintf("--btcd.rpchost=%v", b.rpcConfig.Host)) + args = append(args, fmt.Sprintf("--btcd.rpcuser=%v", b.rpcConfig.User)) + args = append(args, fmt.Sprintf("--btcd.rpcpass=%v", b.rpcConfig.Pass)) + args = append(args, fmt.Sprintf("--btcd.rawrpccert=%v", encodedCert)) + + return args +} + +// P2PAddr returns the address of this node to be used when connection over the +// Bitcoin P2P network. +func (b BtcdBackendConfig) P2PAddr() string { + return b.p2pAddress +} + +// NewBtcdBackend starts a new rpctest.Harness and returns a BtcdBackendConfig +// for that node. +func NewBtcdBackend() (*BtcdBackendConfig, func(), error) { + args := []string{ + "--rejectnonstd", + "--txindex", + "--trickleinterval=100ms", + "--debuglevel=debug", + "--logdir=" + logDir, + } + netParams := &chaincfg.SimNetParams + chainBackend, err := rpctest.New(netParams, nil, args) + if err != nil { + return nil, nil, fmt.Errorf("unable to create btcd node: %v", err) + } + + if err := chainBackend.SetUp(false, 0); err != nil { + return nil, nil, fmt.Errorf("unable to set up btcd backend: %v", err) + } + + bd := &BtcdBackendConfig{ + rpcConfig: chainBackend.RPCConfig(), + p2pAddress: chainBackend.P2PAddress(), + } + + cleanUp := func() { + chainBackend.TearDown() + + // After shutting down the chain backend, we'll make a copy of + // the log file before deleting the temporary log dir. + logFile := logDir + "/" + netParams.Name + "/btcd.log" + err := CopyFile("./output_btcd_chainbackend.log", logFile) + if err != nil { + fmt.Printf("unable to copy file: %v\n", err) + } + if err = os.RemoveAll(logDir); err != nil { + fmt.Printf("Cannot remove dir %s: %v\n", logDir, err) + } + } + + return bd, cleanUp, nil +} diff --git a/lntest/harness.go b/lntest/harness.go index 0a3a9d2e..70ab825c 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -16,7 +16,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/integration/rpctest" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -50,13 +49,16 @@ const ( // The harness by default is created with two active nodes on the network: // Alice and Bob. type NetworkHarness struct { - rpcConfig rpcclient.ConnConfig netParams *chaincfg.Params // Miner is a reference to a running full node that can be used to create // new blocks on the network. Miner *rpctest.Harness + // BackendCfg houses the information necessary to use a node as LND + // chain backend, such as rpc configuration, P2P information etc. + BackendCfg BackendConfig + activeNodes map[int]*HarnessNode nodesByPub map[string]*HarnessNode @@ -82,7 +84,7 @@ type NetworkHarness struct { // TODO(roasbeef): add option to use golang's build library to a binary of the // current repo. This will save developers from having to manually `go install` // within the repo each time before changes -func NewNetworkHarness(r *rpctest.Harness) (*NetworkHarness, error) { +func NewNetworkHarness(r *rpctest.Harness, b BackendConfig) (*NetworkHarness, error) { n := NetworkHarness{ activeNodes: make(map[int]*HarnessNode), nodesByPub: make(map[string]*HarnessNode), @@ -91,7 +93,7 @@ func NewNetworkHarness(r *rpctest.Harness) (*NetworkHarness, error) { lndErrorChan: make(chan error), netParams: r.ActiveNet, Miner: r, - rpcConfig: r.RPCConfig(), + BackendCfg: b, quit: make(chan struct{}), } go n.networkWatcher() @@ -355,11 +357,11 @@ func (n *NetworkHarness) RestoreNodeWithSeed(name string, extraArgs []string, func (n *NetworkHarness) newNode(name string, extraArgs []string, hasSeed bool) (*HarnessNode, error) { node, err := newNode(nodeConfig{ - Name: name, - HasSeed: hasSeed, - RPCConfig: &n.rpcConfig, - NetParams: n.netParams, - ExtraArgs: extraArgs, + Name: name, + HasSeed: hasSeed, + BackendCfg: n.BackendCfg, + NetParams: n.netParams, + ExtraArgs: extraArgs, }) if err != nil { return nil, err diff --git a/lntest/node.go b/lntest/node.go index d518dd95..55581367 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -22,7 +22,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/go-errors/errors" @@ -91,12 +90,24 @@ func generateListeningPorts() (int, int, int) { return p2p, rpc, rest } +// BackendConfig is an interface that abstracts away the specific chain backend +// node implementation. +type BackendConfig interface { + // GenArgs returns the arguments needed to be passed to LND at startup + // for using this node as a chain backend. + GenArgs() []string + + // P2PAddr returns the address of this node to be used when connection + // over the Bitcoin P2P network. + P2PAddr() string +} + type nodeConfig struct { - Name string - RPCConfig *rpcclient.ConnConfig - NetParams *chaincfg.Params - BaseDir string - ExtraArgs []string + Name string + BackendCfg BackendConfig + NetParams *chaincfg.Params + BaseDir string + ExtraArgs []string DataDir string LogDir string @@ -144,16 +155,13 @@ func (cfg nodeConfig) genArgs() []string { args = append(args, "--bitcoin.regtest") } - encodedCert := hex.EncodeToString(cfg.RPCConfig.Certificates) + backendArgs := cfg.BackendCfg.GenArgs() + args = append(args, backendArgs...) args = append(args, "--bitcoin.active") args = append(args, "--nobootstrap") args = append(args, "--debuglevel=debug") args = append(args, "--bitcoin.defaultchanconfs=1") args = append(args, fmt.Sprintf("--bitcoin.defaultremotedelay=%v", DefaultCSV)) - args = append(args, fmt.Sprintf("--btcd.rpchost=%v", cfg.RPCConfig.Host)) - args = append(args, fmt.Sprintf("--btcd.rpcuser=%v", cfg.RPCConfig.User)) - args = append(args, fmt.Sprintf("--btcd.rpcpass=%v", cfg.RPCConfig.Pass)) - args = append(args, fmt.Sprintf("--btcd.rawrpccert=%v", encodedCert)) args = append(args, fmt.Sprintf("--rpclisten=%v", cfg.RPCAddr())) args = append(args, fmt.Sprintf("--restlisten=%v", cfg.RESTAddr())) args = append(args, fmt.Sprintf("--listen=%v", cfg.P2PAddr()))