2016-08-30 07:55:01 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-03-15 01:38:04 +03:00
|
|
|
"bytes"
|
2016-08-30 07:55:01 +03:00
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2017-03-14 08:15:41 +03:00
|
|
|
"io"
|
2016-08-30 07:55:01 +03:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strconv"
|
2017-03-16 22:15:37 +03:00
|
|
|
"strings"
|
2016-08-31 02:44:47 +03:00
|
|
|
"sync"
|
2016-08-30 07:55:01 +03:00
|
|
|
"time"
|
|
|
|
|
2016-08-30 21:12:31 +03:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/grpclog"
|
|
|
|
|
2017-03-14 08:15:41 +03:00
|
|
|
"os/exec"
|
|
|
|
|
2016-10-15 16:47:09 +03:00
|
|
|
"github.com/go-errors/errors"
|
2016-08-30 07:55:01 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
|
|
"github.com/roasbeef/btcd/chaincfg"
|
2017-01-06 00:56:27 +03:00
|
|
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
2016-08-30 07:55:01 +03:00
|
|
|
"github.com/roasbeef/btcd/rpctest"
|
|
|
|
"github.com/roasbeef/btcd/txscript"
|
|
|
|
"github.com/roasbeef/btcd/wire"
|
|
|
|
"github.com/roasbeef/btcrpcclient"
|
|
|
|
"github.com/roasbeef/btcutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// numActiveNodes is the number of active nodes within the test network.
|
|
|
|
numActiveNodes = 0
|
|
|
|
|
|
|
|
// defaultNodePort is the initial p2p port which will be used by the
|
|
|
|
// first created lightning node to listen on for incoming p2p
|
|
|
|
// connections. Subsequent allocated ports for future lighting nodes
|
|
|
|
// instances will be monotonically increasing odd numbers calculated as
|
|
|
|
// such: defaultP2pPort + (2 * harness.nodeNum).
|
|
|
|
defaultNodePort = 19555
|
|
|
|
|
|
|
|
// defaultClientPort is the initial rpc port which will be used by the
|
|
|
|
// first created lightning node to listen on for incoming rpc
|
|
|
|
// connections. Subsequent allocated ports for future rpc harness
|
|
|
|
// instances will be monotonically increasing even numbers calculated
|
|
|
|
// as such: defaultP2pPort + (2 * harness.nodeNum).
|
|
|
|
defaultClientPort = 19556
|
|
|
|
|
|
|
|
harnessNetParams = &chaincfg.SimNetParams
|
|
|
|
)
|
|
|
|
|
|
|
|
// generateListeningPorts returns two strings representing ports to listen on
|
|
|
|
// designated for the current lightning network test. If there haven't been any
|
|
|
|
// test instances created, the default ports are used. Otherwise, in order to
|
|
|
|
// support multiple test nodes running at once, the p2p and rpc port are
|
|
|
|
// incremented after each initialization.
|
|
|
|
func generateListeningPorts() (int, int) {
|
|
|
|
var p2p, rpc int
|
|
|
|
if numActiveNodes == 0 {
|
|
|
|
p2p = defaultNodePort
|
|
|
|
rpc = defaultClientPort
|
|
|
|
} else {
|
|
|
|
p2p = defaultNodePort + (2 * numActiveNodes)
|
|
|
|
rpc = defaultClientPort + (2 * numActiveNodes)
|
|
|
|
}
|
|
|
|
|
|
|
|
return p2p, rpc
|
|
|
|
}
|
|
|
|
|
|
|
|
// lightningNode represents an instance of lnd running within our test network
|
2016-09-10 23:14:28 +03:00
|
|
|
// harness. Each lightningNode instance also fully embedds an RPC client in
|
2016-10-15 16:18:38 +03:00
|
|
|
// order to pragmatically drive the node.
|
2016-08-30 07:55:01 +03:00
|
|
|
type lightningNode struct {
|
|
|
|
cfg *config
|
|
|
|
|
|
|
|
rpcAddr string
|
|
|
|
p2pAddr string
|
|
|
|
rpcCert []byte
|
|
|
|
|
2017-02-23 01:49:04 +03:00
|
|
|
nodeID int
|
2016-08-30 07:55:01 +03:00
|
|
|
|
2016-09-26 20:29:18 +03:00
|
|
|
// PubKey is the serialized compressed identity public key of the node.
|
|
|
|
// This field will only be populated once the node itself has been
|
|
|
|
// started via the start() method.
|
|
|
|
PubKey [33]byte
|
|
|
|
PubKeyStr string
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
cmd *exec.Cmd
|
|
|
|
pidFile string
|
|
|
|
|
2016-11-15 02:49:02 +03:00
|
|
|
// processExit is a channel that's closed once it's detected that the
|
|
|
|
// process this instance of lightningNode is bound to has exited.
|
|
|
|
processExit chan struct{}
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
extraArgs []string
|
2016-09-10 23:14:28 +03:00
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
chanWatchRequests chan *chanWatchRequest
|
|
|
|
|
|
|
|
quit chan struct{}
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
2016-09-10 23:14:28 +03:00
|
|
|
lnrpc.LightningClient
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// newLightningNode creates a new test lightning node instance from the passed
|
|
|
|
// rpc config and slice of extra arguments.
|
2017-05-03 05:47:17 +03:00
|
|
|
func newLightningNode(btcrpcConfig *btcrpcclient.ConnConfig, lndArgs []string) (*lightningNode, error) {
|
2016-08-30 07:55:01 +03:00
|
|
|
var err error
|
|
|
|
|
|
|
|
cfg := &config{
|
2017-05-03 05:47:17 +03:00
|
|
|
Bitcoin: &chainConfig{
|
|
|
|
RPCHost: btcrpcConfig.Host,
|
|
|
|
RPCUser: btcrpcConfig.User,
|
|
|
|
RPCPass: btcrpcConfig.Pass,
|
|
|
|
},
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
nodeNum := numActiveNodes
|
|
|
|
cfg.DataDir, err = ioutil.TempDir("", "lndtest-data")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cfg.LogDir, err = ioutil.TempDir("", "lndtest-log")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg.PeerPort, cfg.RPCPort = generateListeningPorts()
|
|
|
|
|
|
|
|
numActiveNodes++
|
|
|
|
|
2017-03-25 11:40:33 +03:00
|
|
|
lndArgs = append(lndArgs, "--externalip=127.0.0.1:"+
|
|
|
|
strconv.Itoa(cfg.PeerPort))
|
|
|
|
|
2016-11-22 06:08:44 +03:00
|
|
|
return &lightningNode{
|
2017-03-15 01:38:04 +03:00
|
|
|
cfg: cfg,
|
|
|
|
p2pAddr: net.JoinHostPort("127.0.0.1", strconv.Itoa(cfg.PeerPort)),
|
|
|
|
rpcAddr: net.JoinHostPort("127.0.0.1", strconv.Itoa(cfg.RPCPort)),
|
2017-05-03 05:47:17 +03:00
|
|
|
rpcCert: btcrpcConfig.Certificates,
|
2017-03-15 01:38:04 +03:00
|
|
|
nodeID: nodeNum,
|
|
|
|
chanWatchRequests: make(chan *chanWatchRequest),
|
|
|
|
processExit: make(chan struct{}),
|
|
|
|
quit: make(chan struct{}),
|
|
|
|
extraArgs: lndArgs,
|
2016-08-30 07:55:01 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// genArgs generates a slice of command line arguments from the lightningNode's
|
|
|
|
// current config struct.
|
|
|
|
func (l *lightningNode) genArgs() []string {
|
|
|
|
var args []string
|
|
|
|
|
|
|
|
encodedCert := hex.EncodeToString(l.rpcCert)
|
2017-05-03 05:47:17 +03:00
|
|
|
args = append(args, "--bitcoin.active")
|
|
|
|
args = append(args, "--bitcoin.simnet")
|
|
|
|
args = append(args, fmt.Sprintf("--bitcoin.rpchost=%v", l.cfg.Bitcoin.RPCHost))
|
|
|
|
args = append(args, fmt.Sprintf("--bitcoin.rpcuser=%v", l.cfg.Bitcoin.RPCUser))
|
|
|
|
args = append(args, fmt.Sprintf("--bitcoin.rpcpass=%v", l.cfg.Bitcoin.RPCPass))
|
|
|
|
args = append(args, fmt.Sprintf("--bitcoin.rawrpccert=%v", encodedCert))
|
2016-08-30 07:55:01 +03:00
|
|
|
args = append(args, fmt.Sprintf("--rpcport=%v", l.cfg.RPCPort))
|
|
|
|
args = append(args, fmt.Sprintf("--peerport=%v", l.cfg.PeerPort))
|
|
|
|
args = append(args, fmt.Sprintf("--logdir=%v", l.cfg.LogDir))
|
|
|
|
args = append(args, fmt.Sprintf("--datadir=%v", l.cfg.DataDir))
|
|
|
|
|
|
|
|
if l.extraArgs != nil {
|
|
|
|
args = append(args, l.extraArgs...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return args
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Start launches a new process running lnd. Additionally, the PID of the
|
2016-08-30 07:55:01 +03:00
|
|
|
// launched process is saved in order to possibly kill the process forcibly
|
|
|
|
// later.
|
2017-03-15 01:38:04 +03:00
|
|
|
func (l *lightningNode) Start(lndError chan error) error {
|
2016-08-30 07:55:01 +03:00
|
|
|
args := l.genArgs()
|
|
|
|
|
|
|
|
l.cmd = exec.Command("lnd", args...)
|
2016-10-15 16:47:09 +03:00
|
|
|
|
|
|
|
// Redirect stderr output to buffer
|
|
|
|
var errb bytes.Buffer
|
|
|
|
l.cmd.Stderr = &errb
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
if err := l.cmd.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-10-24 05:00:09 +03:00
|
|
|
// Launch a new goroutine which that bubbles up any potential fatal
|
|
|
|
// process errors to the goroutine running the tests.
|
2016-10-15 16:47:09 +03:00
|
|
|
go func() {
|
2017-02-24 14:36:10 +03:00
|
|
|
err := l.cmd.Wait()
|
2016-11-15 02:49:02 +03:00
|
|
|
|
|
|
|
// Signal any onlookers that this process has exited.
|
|
|
|
close(l.processExit)
|
2017-02-24 14:36:10 +03:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
lndError <- errors.New(errb.String())
|
|
|
|
}
|
2016-10-15 16:47:09 +03:00
|
|
|
}()
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
pid, err := os.Create(filepath.Join(l.cfg.DataDir,
|
2017-02-23 01:49:04 +03:00
|
|
|
fmt.Sprintf("%v.pid", l.nodeID)))
|
2016-08-30 07:55:01 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
l.pidFile = pid.Name()
|
2016-11-22 06:08:44 +03:00
|
|
|
if _, err = fmt.Fprintf(pid, "%v\n", l.cmd.Process.Pid); err != nil {
|
2016-08-30 07:55:01 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := pid.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-10 23:14:28 +03:00
|
|
|
opts := []grpc.DialOption{
|
|
|
|
grpc.WithInsecure(),
|
|
|
|
grpc.WithBlock(),
|
|
|
|
grpc.WithTimeout(time.Second * 20),
|
|
|
|
}
|
|
|
|
conn, err := grpc.Dial(l.rpcAddr, opts...)
|
|
|
|
if err != nil {
|
2016-11-16 23:44:39 +03:00
|
|
|
return err
|
2016-09-10 23:14:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
l.LightningClient = lnrpc.NewLightningClient(conn)
|
|
|
|
|
2016-09-14 01:38:37 +03:00
|
|
|
// Obtain the lnid of this node for quick identification purposes.
|
|
|
|
ctxb := context.Background()
|
|
|
|
info, err := l.GetInfo(ctxb, &lnrpc.GetInfoRequest{})
|
|
|
|
if err != nil {
|
2016-11-16 23:44:39 +03:00
|
|
|
return err
|
2016-09-14 01:38:37 +03:00
|
|
|
}
|
2016-09-26 20:29:18 +03:00
|
|
|
|
|
|
|
l.PubKeyStr = info.IdentityPubkey
|
|
|
|
|
|
|
|
pubkey, err := hex.DecodeString(info.IdentityPubkey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
copy(l.PubKey[:], pubkey)
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Launch the watcher that'll hook into graph related topology change
|
|
|
|
// from the PoV of this node.
|
|
|
|
l.wg.Add(1)
|
|
|
|
go l.lightningNetworkWatcher()
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// cleanup cleans up all the temporary files created by the node's process.
|
|
|
|
func (l *lightningNode) cleanup() error {
|
|
|
|
dirs := []string{
|
|
|
|
l.cfg.LogDir,
|
|
|
|
l.cfg.DataDir,
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
for _, dir := range dirs {
|
|
|
|
if err = os.RemoveAll(dir); err != nil {
|
|
|
|
log.Printf("Cannot remove dir %s: %v", dir, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Stop attempts to stop the active lnd process.
|
|
|
|
func (l *lightningNode) Stop() error {
|
2016-10-15 16:47:09 +03:00
|
|
|
// We should skip node stop in case:
|
|
|
|
// - start of the node wasn't initiated
|
|
|
|
// - process wasn't spawned
|
|
|
|
// - process already finished
|
2017-02-24 14:36:10 +03:00
|
|
|
select {
|
2017-04-17 01:24:48 +03:00
|
|
|
case <-l.quit:
|
|
|
|
return nil
|
2017-02-24 14:36:10 +03:00
|
|
|
case <-l.processExit:
|
|
|
|
return nil
|
|
|
|
default:
|
2017-04-17 01:24:48 +03:00
|
|
|
}
|
2017-03-15 01:38:04 +03:00
|
|
|
|
2017-04-17 01:24:48 +03:00
|
|
|
if runtime.GOOS == "windows" {
|
2017-04-18 02:09:47 +03:00
|
|
|
if err := l.cmd.Process.Signal(os.Kill); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else if err := l.cmd.Process.Signal(os.Interrupt); err != nil {
|
|
|
|
return err
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
2017-04-18 02:09:47 +03:00
|
|
|
|
|
|
|
close(l.quit)
|
|
|
|
l.wg.Wait()
|
|
|
|
return nil
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Restart attempts to restart a lightning node by shutting it down cleanly,
|
2016-11-15 02:49:02 +03:00
|
|
|
// then restarting the process. This function is fully blocking. Upon restart,
|
|
|
|
// the RPC connection to the node will be re-attempted, continuing iff the
|
2016-11-22 06:08:44 +03:00
|
|
|
// connection attempt is successful. Additionally, if a callback is passed, the
|
|
|
|
// closure will be executed after the node has been shutdown, but before the
|
|
|
|
// process has been started up again.
|
2017-03-15 01:38:04 +03:00
|
|
|
func (l *lightningNode) Restart(errChan chan error, callback func() error) error {
|
|
|
|
if err := l.Stop(); err != nil {
|
2016-11-15 02:49:02 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
<-l.processExit
|
|
|
|
|
|
|
|
l.processExit = make(chan struct{})
|
2017-03-15 01:38:04 +03:00
|
|
|
l.quit = make(chan struct{})
|
|
|
|
l.wg = sync.WaitGroup{}
|
2016-11-15 02:49:02 +03:00
|
|
|
|
2016-11-22 06:08:44 +03:00
|
|
|
if callback != nil {
|
|
|
|
if err := callback(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
return l.Start(errChan)
|
2016-11-15 02:49:02 +03:00
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Shutdown stops the active lnd process and clean up any temporary directories
|
2016-08-30 07:55:01 +03:00
|
|
|
// created along the way.
|
2017-03-15 01:38:04 +03:00
|
|
|
func (l *lightningNode) Shutdown() error {
|
|
|
|
if err := l.Stop(); err != nil {
|
2016-08-30 07:55:01 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := l.cleanup(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// closeChanWatchRequest is a request to the lightningNetworkWatcher to be
|
|
|
|
// notified once it's detected within the test Lightning Network, that a
|
|
|
|
// channel has either been added or closed.
|
|
|
|
type chanWatchRequest struct {
|
|
|
|
chanPoint wire.OutPoint
|
|
|
|
|
|
|
|
chanOpen bool
|
|
|
|
|
|
|
|
eventChan chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// lightningNetworkWatcher is a goroutine which is able to dispatch
|
|
|
|
// notifications once it has been observed that a target channel has been
|
|
|
|
// closed or opened within the network. In order to dispatch these
|
|
|
|
// notifications, the GraphTopologySubscription client exposed as part of the
|
|
|
|
// gRPC interface is used.
|
|
|
|
func (l *lightningNode) lightningNetworkWatcher() {
|
|
|
|
defer l.wg.Done()
|
|
|
|
|
2017-03-16 22:15:37 +03:00
|
|
|
// If the channel router is shutting down, then we won't consider it as
|
|
|
|
// a real error. This just indicates the daemon itself is quitting.
|
|
|
|
isShutdownError := func(err error) bool {
|
|
|
|
return strings.Contains(err.Error(), "shutting down")
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
graphUpdates := make(chan *lnrpc.GraphTopologyUpdate)
|
2017-04-18 02:06:43 +03:00
|
|
|
l.wg.Add(1)
|
2017-03-15 01:38:04 +03:00
|
|
|
go func() {
|
2017-04-18 02:06:43 +03:00
|
|
|
defer l.wg.Done()
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
ctxb := context.Background()
|
|
|
|
req := &lnrpc.GraphTopologySubscription{}
|
|
|
|
topologyClient, err := l.SubscribeChannelGraph(ctxb, req)
|
|
|
|
if err != nil {
|
|
|
|
// We panic here in case of an error as failure to
|
|
|
|
// create the topology client will cause all subsequent
|
|
|
|
// tests to fail.
|
|
|
|
panic(fmt.Errorf("unable to create topology "+
|
|
|
|
"client: %v", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
update, err := topologyClient.Recv()
|
|
|
|
if err == io.EOF {
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
2017-03-16 22:15:37 +03:00
|
|
|
// If the node has been signalled to quit, then
|
|
|
|
// we'll exit early.
|
|
|
|
select {
|
|
|
|
case <-l.quit:
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, if the node is shutting down on
|
|
|
|
// it's own, then we'll also bail out early.
|
|
|
|
if isShutdownError(err) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
// Similar to the case above, we also panic
|
|
|
|
// here (and end the tests) as these
|
|
|
|
// notifications are critical to the success of
|
|
|
|
// many tests.
|
|
|
|
panic(fmt.Errorf("unable read update ntfn: %v", err))
|
|
|
|
}
|
|
|
|
|
2017-04-21 02:06:35 +03:00
|
|
|
select {
|
|
|
|
case graphUpdates <- update:
|
|
|
|
case <-l.quit:
|
|
|
|
return
|
|
|
|
}
|
2017-03-15 01:38:04 +03:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// For each outpoint, we'll track an integer which denotes the number
|
|
|
|
// of edges seen for that channel within the network. When this number
|
|
|
|
// reaches 2, then it means that both edge advertisements has
|
|
|
|
// propagated through the network.
|
|
|
|
openChans := make(map[wire.OutPoint]int)
|
|
|
|
openClients := make(map[wire.OutPoint][]chan struct{})
|
|
|
|
|
|
|
|
closedChans := make(map[wire.OutPoint]struct{})
|
|
|
|
closeClients := make(map[wire.OutPoint][]chan struct{})
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
|
|
|
|
// A new graph update has just been received, so we'll examine
|
|
|
|
// the current set of registered clients to see if we can
|
|
|
|
// dispatch any requests.
|
|
|
|
case graphUpdate := <-graphUpdates:
|
|
|
|
// For each new channel, we'll increment the number of
|
|
|
|
// edges seen by one.
|
|
|
|
for _, newChan := range graphUpdate.ChannelUpdates {
|
|
|
|
txid, _ := chainhash.NewHash(newChan.ChanPoint.FundingTxid)
|
|
|
|
op := wire.OutPoint{
|
|
|
|
Hash: *txid,
|
|
|
|
Index: newChan.ChanPoint.OutputIndex,
|
|
|
|
}
|
|
|
|
openChans[op]++
|
|
|
|
|
|
|
|
// For this new channel, if the number of edges
|
|
|
|
// seen is less than two, then the channel
|
|
|
|
// hasn't been fully announced yet.
|
|
|
|
if numEdges := openChans[op]; numEdges < 2 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, we'll notify all the registered
|
|
|
|
// clients and remove the dispatched clients.
|
|
|
|
for _, eventChan := range openClients[op] {
|
|
|
|
close(eventChan)
|
|
|
|
}
|
|
|
|
delete(openClients, op)
|
|
|
|
}
|
|
|
|
|
|
|
|
// For each channel closed, we'll mark that we've
|
|
|
|
// detected a channel closure while lnd was pruning the
|
|
|
|
// channel graph.
|
|
|
|
for _, closedChan := range graphUpdate.ClosedChans {
|
|
|
|
txid, _ := chainhash.NewHash(closedChan.ChanPoint.FundingTxid)
|
|
|
|
op := wire.OutPoint{
|
|
|
|
Hash: *txid,
|
|
|
|
Index: closedChan.ChanPoint.OutputIndex,
|
|
|
|
}
|
|
|
|
closedChans[op] = struct{}{}
|
|
|
|
|
|
|
|
// As the channel has been closed, we'll notify
|
|
|
|
// all register clients.
|
|
|
|
for _, eventChan := range closeClients[op] {
|
|
|
|
close(eventChan)
|
|
|
|
}
|
|
|
|
delete(closeClients, op)
|
|
|
|
}
|
|
|
|
|
|
|
|
// A new watch request, has just arrived. We'll either be able
|
|
|
|
// to dispatch immediately, or need to add the client for
|
|
|
|
// processing later.
|
|
|
|
case watchRequest := <-l.chanWatchRequests:
|
|
|
|
targetChan := watchRequest.chanPoint
|
|
|
|
|
|
|
|
// TODO(roasbeef): add update type also, checks for
|
|
|
|
// multiple of 2
|
|
|
|
if watchRequest.chanOpen {
|
|
|
|
// If this is a open request, then it can be
|
|
|
|
// dispatched if the number of edges seen for
|
|
|
|
// the channel is at least two.
|
2017-03-16 14:04:19 +03:00
|
|
|
if numEdges := openChans[targetChan]; numEdges >= 2 {
|
2017-03-15 01:38:04 +03:00
|
|
|
close(watchRequest.eventChan)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, we'll add this to the list of
|
|
|
|
// watch open clients for this out point.
|
|
|
|
openClients[targetChan] = append(openClients[targetChan],
|
|
|
|
watchRequest.eventChan)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is a close request, then it can be
|
|
|
|
// immediately dispatched if we've already seen a
|
|
|
|
// channel closure for this channel.
|
|
|
|
if _, ok := closedChans[targetChan]; ok {
|
|
|
|
close(watchRequest.eventChan)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, we'll add this to the list of close watch
|
|
|
|
// clients for this out point.
|
|
|
|
closeClients[targetChan] = append(closeClients[targetChan],
|
|
|
|
watchRequest.eventChan)
|
|
|
|
|
|
|
|
case <-l.quit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitForNetworkChannelOpen will block until a channel with the target
|
|
|
|
// outpoint is seen as being fully advertised within the network. A channel is
|
|
|
|
// considered "fully advertised" once both of its directional edges has been
|
|
|
|
// advertised within the test Lightning Network.
|
|
|
|
func (l *lightningNode) WaitForNetworkChannelOpen(ctx context.Context,
|
|
|
|
op *lnrpc.ChannelPoint) error {
|
|
|
|
|
|
|
|
eventChan := make(chan struct{})
|
|
|
|
|
|
|
|
txid, err := chainhash.NewHash(op.FundingTxid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
l.chanWatchRequests <- &chanWatchRequest{
|
|
|
|
chanPoint: wire.OutPoint{
|
|
|
|
Hash: *txid,
|
|
|
|
Index: op.OutputIndex,
|
|
|
|
},
|
|
|
|
eventChan: eventChan,
|
|
|
|
chanOpen: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-eventChan:
|
|
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fmt.Errorf("channel not opened before timeout")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitForNetworkChannelClose will block until a channel with the target
|
|
|
|
// outpoint is seen as closed within the network. A channel is considered
|
|
|
|
// closed once a transaction spending the funding outpoint is seen within a
|
|
|
|
// confirmed block.
|
|
|
|
func (l *lightningNode) WaitForNetworkChannelClose(ctx context.Context,
|
|
|
|
op *lnrpc.ChannelPoint) error {
|
|
|
|
|
|
|
|
eventChan := make(chan struct{})
|
|
|
|
|
|
|
|
txid, err := chainhash.NewHash(op.FundingTxid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
l.chanWatchRequests <- &chanWatchRequest{
|
|
|
|
chanPoint: wire.OutPoint{
|
|
|
|
Hash: *txid,
|
|
|
|
Index: op.OutputIndex,
|
|
|
|
},
|
|
|
|
eventChan: eventChan,
|
|
|
|
chanOpen: false,
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-eventChan:
|
|
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fmt.Errorf("channel not closed before timeout")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
// networkHarness is an integration testing harness for the lightning network.
|
|
|
|
// The harness by default is created with two active nodes on the network:
|
|
|
|
// Alice and Bob.
|
|
|
|
type networkHarness struct {
|
|
|
|
rpcConfig btcrpcclient.ConnConfig
|
|
|
|
netParams *chaincfg.Params
|
|
|
|
Miner *rpctest.Harness
|
|
|
|
|
|
|
|
activeNodes map[int]*lightningNode
|
|
|
|
|
2016-09-10 23:14:28 +03:00
|
|
|
// Alice and Bob are the initial seeder nodes that are automatically
|
|
|
|
// created to be the initial participants of the test network.
|
|
|
|
Alice *lightningNode
|
|
|
|
Bob *lightningNode
|
2016-08-31 05:34:13 +03:00
|
|
|
|
2017-03-14 08:15:41 +03:00
|
|
|
seenTxns chan chainhash.Hash
|
|
|
|
bitcoinWatchRequests chan *txWatchRequest
|
|
|
|
|
2016-10-15 16:47:09 +03:00
|
|
|
// Channel for transmitting stderr output from failed lightning node
|
|
|
|
// to main process.
|
|
|
|
lndErrorChan chan error
|
|
|
|
|
2016-09-26 20:30:24 +03:00
|
|
|
sync.Mutex
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2016-09-15 21:59:51 +03:00
|
|
|
// newNetworkHarness creates a new network test harness.
|
2016-08-30 08:07:54 +03:00
|
|
|
// 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`
|
2016-09-15 21:59:51 +03:00
|
|
|
// within the repo each time before changes
|
|
|
|
func newNetworkHarness() (*networkHarness, error) {
|
2016-08-31 05:34:13 +03:00
|
|
|
return &networkHarness{
|
2017-03-14 08:15:41 +03:00
|
|
|
activeNodes: make(map[int]*lightningNode),
|
|
|
|
seenTxns: make(chan chainhash.Hash),
|
|
|
|
bitcoinWatchRequests: make(chan *txWatchRequest),
|
|
|
|
lndErrorChan: make(chan error),
|
2016-08-31 05:34:13 +03:00
|
|
|
}, nil
|
|
|
|
}
|
2016-08-30 07:55:01 +03:00
|
|
|
|
2016-09-15 21:59:51 +03:00
|
|
|
// InitializeSeedNodes initialized alice and bob nodes given an already
|
|
|
|
// running instance of btcd's rpctest harness and extra command line flags,
|
|
|
|
// which should be formatted properly - "--arg=value".
|
|
|
|
func (n *networkHarness) InitializeSeedNodes(r *rpctest.Harness, lndArgs []string) error {
|
2016-08-30 07:55:01 +03:00
|
|
|
nodeConfig := r.RPCConfig()
|
|
|
|
|
2016-08-31 05:34:13 +03:00
|
|
|
n.netParams = r.ActiveNet
|
|
|
|
n.Miner = r
|
|
|
|
n.rpcConfig = nodeConfig
|
2016-08-30 07:55:01 +03:00
|
|
|
|
2016-08-31 05:34:13 +03:00
|
|
|
var err error
|
2016-09-15 21:59:51 +03:00
|
|
|
n.Alice, err = newLightningNode(&nodeConfig, lndArgs)
|
2016-08-30 07:55:01 +03:00
|
|
|
if err != nil {
|
2016-08-31 05:34:13 +03:00
|
|
|
return err
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
2016-09-15 21:59:51 +03:00
|
|
|
n.Bob, err = newLightningNode(&nodeConfig, lndArgs)
|
2016-08-30 07:55:01 +03:00
|
|
|
if err != nil {
|
2016-08-31 05:34:13 +03:00
|
|
|
return err
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2017-02-23 01:49:04 +03:00
|
|
|
n.activeNodes[n.Alice.nodeID] = n.Alice
|
|
|
|
n.activeNodes[n.Bob.nodeID] = n.Bob
|
2016-08-30 07:55:01 +03:00
|
|
|
|
2016-08-31 05:34:13 +03:00
|
|
|
return err
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2016-10-24 05:00:09 +03:00
|
|
|
// ProcessErrors returns a channel used for reporting any fatal process errors.
|
|
|
|
// If any of the active nodes within the harness' test network incur a fatal
|
|
|
|
// error, that error is sent over this channel.
|
|
|
|
func (n *networkHarness) ProcessErrors() chan error {
|
|
|
|
return n.lndErrorChan
|
|
|
|
}
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
// fakeLogger is a fake grpclog.Logger implementation. This is used to stop
|
|
|
|
// grpc's logger from printing directly to stdout.
|
|
|
|
type fakeLogger struct{}
|
|
|
|
|
|
|
|
func (f *fakeLogger) Fatal(args ...interface{}) {}
|
|
|
|
func (f *fakeLogger) Fatalf(format string, args ...interface{}) {}
|
|
|
|
func (f *fakeLogger) Fatalln(args ...interface{}) {}
|
|
|
|
func (f *fakeLogger) Print(args ...interface{}) {}
|
|
|
|
func (f *fakeLogger) Printf(format string, args ...interface{}) {}
|
|
|
|
func (f *fakeLogger) Println(args ...interface{}) {}
|
|
|
|
|
|
|
|
// SetUp starts the initial seeder nodes within the test harness. The initial
|
|
|
|
// node's wallets will be funded wallets with ten 1 BTC outputs each. Finally
|
|
|
|
// rpc clients capable of communicating with the initial seeder nodes are
|
|
|
|
// created.
|
|
|
|
func (n *networkHarness) SetUp() error {
|
|
|
|
// Swap out grpc's default logger with out fake logger which drops the
|
|
|
|
// statements on the floor.
|
|
|
|
grpclog.SetLogger(&fakeLogger{})
|
|
|
|
// Start the initial seeder nodes within the test network, then connect
|
|
|
|
// their respective RPC clients.
|
2016-08-31 02:44:47 +03:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
errChan := make(chan error, 2)
|
|
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
|
|
var err error
|
|
|
|
defer wg.Done()
|
2017-03-15 01:38:04 +03:00
|
|
|
if err = n.Alice.Start(n.lndErrorChan); err != nil {
|
2016-08-31 02:44:47 +03:00
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
var err error
|
|
|
|
defer wg.Done()
|
2017-03-15 01:38:04 +03:00
|
|
|
if err = n.Bob.Start(n.lndErrorChan); err != nil {
|
2016-08-31 02:44:47 +03:00
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
select {
|
|
|
|
case err := <-errChan:
|
2016-08-30 07:55:01 +03:00
|
|
|
return err
|
2016-08-31 02:44:47 +03:00
|
|
|
default:
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Load up the wallets of the seeder nodes with 10 outputs of 1 BTC
|
|
|
|
// each.
|
|
|
|
ctxb := context.Background()
|
2017-02-10 02:28:32 +03:00
|
|
|
addrReq := &lnrpc.NewAddressRequest{
|
|
|
|
Type: lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH,
|
|
|
|
}
|
2016-09-10 23:14:28 +03:00
|
|
|
clients := []lnrpc.LightningClient{n.Alice, n.Bob}
|
2016-08-30 07:55:01 +03:00
|
|
|
for _, client := range clients {
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
resp, err := client.NewAddress(ctxb, addrReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addr, err := btcutil.DecodeAddress(resp.Address, n.netParams)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addrScript, err := txscript.PayToAddrScript(addr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
output := &wire.TxOut{
|
|
|
|
PkScript: addrScript,
|
|
|
|
Value: btcutil.SatoshiPerBitcoin,
|
|
|
|
}
|
2017-01-06 00:56:27 +03:00
|
|
|
if _, err := n.Miner.SendOutputs([]*wire.TxOut{output}, 30); err != nil {
|
2016-08-30 07:55:01 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We generate several blocks in order to give the outputs created
|
|
|
|
// above a good number of confirmations.
|
|
|
|
if _, err := n.Miner.Node.Generate(10); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, make a connection between both of the nodes.
|
2016-09-26 20:31:07 +03:00
|
|
|
if err := n.ConnectNodes(ctxb, n.Alice, n.Bob); err != nil {
|
2016-08-30 07:55:01 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-08-31 02:44:47 +03:00
|
|
|
// Now block until both wallets have fully synced up.
|
|
|
|
expectedBalance := btcutil.Amount(btcutil.SatoshiPerBitcoin * 10).ToBTC()
|
|
|
|
balReq := &lnrpc.WalletBalanceRequest{}
|
2016-09-26 21:54:13 +03:00
|
|
|
balanceTicker := time.Tick(time.Millisecond * 50)
|
2016-08-31 02:44:47 +03:00
|
|
|
out:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-balanceTicker:
|
2016-09-10 23:14:28 +03:00
|
|
|
aliceResp, err := n.Alice.WalletBalance(ctxb, balReq)
|
2016-08-31 02:44:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-09-10 23:14:28 +03:00
|
|
|
bobResp, err := n.Bob.WalletBalance(ctxb, balReq)
|
2016-08-31 02:44:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if aliceResp.Balance == expectedBalance &&
|
|
|
|
bobResp.Balance == expectedBalance {
|
|
|
|
break out
|
|
|
|
}
|
2016-09-18 03:34:39 +03:00
|
|
|
case <-time.After(time.Second * 30):
|
|
|
|
return fmt.Errorf("balances not synced after deadline")
|
2016-08-31 02:44:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-31 21:59:08 +03:00
|
|
|
// Now that the initial test network has been initialized, launch the
|
2017-04-18 02:06:43 +03:00
|
|
|
// network watcher.
|
2016-08-31 05:34:13 +03:00
|
|
|
go n.networkWatcher()
|
|
|
|
|
2016-08-30 07:55:01 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-08-31 21:59:08 +03:00
|
|
|
// TearDownAll tears down all active nodes within the test lightning network.
|
|
|
|
func (n *networkHarness) TearDownAll() error {
|
|
|
|
for _, node := range n.activeNodes {
|
2017-03-15 01:38:04 +03:00
|
|
|
if err := node.Shutdown(); err != nil {
|
2016-08-31 21:59:08 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-26 20:30:24 +03:00
|
|
|
// NewNode fully initializes a returns a new lightningNode binded to the
|
|
|
|
// current instance of the network harness. The created node is running, but
|
|
|
|
// not yet connected to other nodes within the network.
|
|
|
|
func (n *networkHarness) NewNode(extraArgs []string) (*lightningNode, error) {
|
|
|
|
n.Lock()
|
|
|
|
defer n.Unlock()
|
|
|
|
|
|
|
|
node, err := newLightningNode(&n.rpcConfig, extraArgs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:38:04 +03:00
|
|
|
if err := node.Start(n.lndErrorChan); err != nil {
|
2016-09-26 20:30:24 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-02-23 01:49:04 +03:00
|
|
|
n.activeNodes[node.nodeID] = node
|
2016-09-26 20:30:24 +03:00
|
|
|
|
|
|
|
return node, nil
|
|
|
|
}
|
2016-09-26 20:31:07 +03:00
|
|
|
|
|
|
|
// ConnectNodes establishes an encrypted+authenticated p2p connection from node
|
2017-04-14 01:11:20 +03:00
|
|
|
// a towards node b. The function will return a non-nil error if the connection
|
|
|
|
// was unable to be established.
|
|
|
|
//
|
|
|
|
// NOTE: This function may block for up to 15-seconds as it will not return
|
|
|
|
// until the new connection is detected as being known to both nodes.
|
2016-09-26 20:31:07 +03:00
|
|
|
func (n *networkHarness) ConnectNodes(ctx context.Context, a, b *lightningNode) error {
|
|
|
|
bobInfo, err := b.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &lnrpc.ConnectPeerRequest{
|
|
|
|
Addr: &lnrpc.LightningAddress{
|
2016-10-28 05:43:31 +03:00
|
|
|
Pubkey: bobInfo.IdentityPubkey,
|
|
|
|
Host: b.p2pAddr,
|
2016-09-26 20:31:07 +03:00
|
|
|
},
|
|
|
|
}
|
2017-04-14 01:11:20 +03:00
|
|
|
if _, err := a.ConnectPeer(ctx, req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
timeout := time.After(time.Second * 15)
|
|
|
|
for {
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-timeout:
|
|
|
|
return fmt.Errorf("peers not connected within 15 seconds")
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// If node B is seen in the ListPeers response from node A,
|
|
|
|
// then we can exit early as the connection has been fully
|
|
|
|
// established.
|
|
|
|
resp, err := a.ListPeers(ctx, &lnrpc.ListPeersRequest{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, peer := range resp.Peers {
|
|
|
|
if peer.PubKey == b.PubKeyStr {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-09-26 20:31:07 +03:00
|
|
|
}
|
|
|
|
|
2017-05-05 10:00:39 +03:00
|
|
|
// DisconnectNodes disconnects node a from node b by sending RPC message
|
|
|
|
// from a node to b node
|
|
|
|
func (n *networkHarness) DisconnectNodes(ctx context.Context, a, b *lightningNode) error {
|
|
|
|
bobInfo, err := b.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &lnrpc.DisconnectPeerRequest{
|
|
|
|
PubKey: bobInfo.IdentityPubkey,
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := a.DisconnectPeer(ctx, req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-11-15 02:49:02 +03:00
|
|
|
// RestartNode attempts to restart a lightning node by shutting it down
|
|
|
|
// cleanly, then restarting the process. This function is fully blocking. Upon
|
|
|
|
// restart, the RPC connection to the node will be re-attempted, continuing iff
|
2016-11-22 06:08:44 +03:00
|
|
|
// the connection attempt is successful. If the callback parameter is non-nil,
|
|
|
|
// then the function will be executed after the node shuts down, but *before*
|
|
|
|
// the process has been started up again.
|
2016-11-15 02:49:02 +03:00
|
|
|
//
|
|
|
|
// This method can be useful when testing edge cases such as a node broadcast
|
|
|
|
// and invalidated prior state, or persistent state recovery, simulating node
|
|
|
|
// crashes, etc.
|
2016-11-22 06:08:44 +03:00
|
|
|
func (n *networkHarness) RestartNode(node *lightningNode, callback func() error) error {
|
2017-03-15 01:38:04 +03:00
|
|
|
return node.Restart(n.lndErrorChan, callback)
|
2016-11-15 02:49:02 +03:00
|
|
|
}
|
|
|
|
|
2016-09-26 20:31:07 +03:00
|
|
|
// TODO(roasbeef): add a WithChannel higher-order function?
|
|
|
|
// * python-like context manager w.r.t using a channel within a test
|
|
|
|
// * possibly adds more funds to the target wallet if the funds are not
|
|
|
|
// enough
|
|
|
|
|
2017-03-14 08:15:41 +03:00
|
|
|
// txWatchRequest encapsulates a request to the harness' Bitcoin network
|
|
|
|
// watcher to dispatch a notification once a transaction with the target txid
|
|
|
|
// is seen within the test network.
|
|
|
|
type txWatchRequest struct {
|
2017-01-06 00:56:27 +03:00
|
|
|
txid chainhash.Hash
|
2016-08-31 05:34:13 +03:00
|
|
|
eventChan chan struct{}
|
|
|
|
}
|
|
|
|
|
2017-03-14 08:15:41 +03:00
|
|
|
// bitcoinNetworkWatcher is a goroutine which accepts async notification
|
|
|
|
// requests for the broadcast of a target transaction, and then dispatches the
|
|
|
|
// transaction once its seen on the Bitcoin network.
|
2016-08-31 05:34:13 +03:00
|
|
|
func (n *networkHarness) networkWatcher() {
|
2017-01-06 00:56:27 +03:00
|
|
|
seenTxns := make(map[chainhash.Hash]struct{})
|
|
|
|
clients := make(map[chainhash.Hash][]chan struct{})
|
2016-08-31 05:34:13 +03:00
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
|
|
select {
|
2017-03-14 08:15:41 +03:00
|
|
|
case req := <-n.bitcoinWatchRequests:
|
2016-08-31 05:34:13 +03:00
|
|
|
// If we've already seen this transaction, then
|
|
|
|
// immediately dispatch the request. Otherwise, append
|
|
|
|
// to the list of clients who are watching for the
|
|
|
|
// broadcast of this transaction.
|
|
|
|
if _, ok := seenTxns[req.txid]; ok {
|
|
|
|
close(req.eventChan)
|
|
|
|
} else {
|
|
|
|
clients[req.txid] = append(clients[req.txid], req.eventChan)
|
|
|
|
}
|
|
|
|
case txid := <-n.seenTxns:
|
2016-09-14 04:56:35 +03:00
|
|
|
// Add this txid to our set of "seen" transactions. So
|
|
|
|
// we're able to dispatch any notifications for this
|
|
|
|
// txid which arrive *after* it's seen within the
|
|
|
|
// network.
|
|
|
|
seenTxns[txid] = struct{}{}
|
2016-08-31 05:34:13 +03:00
|
|
|
|
|
|
|
// If there isn't a registered notification for this
|
|
|
|
// transaction then ignore it.
|
|
|
|
txClients, ok := clients[txid]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, dispatch the notification to all clients,
|
|
|
|
// cleaning up the now un-needed state.
|
|
|
|
for _, client := range txClients {
|
|
|
|
close(client)
|
|
|
|
}
|
|
|
|
delete(clients, txid)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-31 21:59:08 +03:00
|
|
|
// OnTxAccepted is a callback to be called each time a new transaction has been
|
|
|
|
// broadcast on the network.
|
2017-01-06 00:56:27 +03:00
|
|
|
func (n *networkHarness) OnTxAccepted(hash *chainhash.Hash, amt btcutil.Amount) {
|
2016-08-31 05:34:13 +03:00
|
|
|
go func() {
|
|
|
|
n.seenTxns <- *hash
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
// WaitForTxBroadcast blocks until the target txid is seen on the network. If
|
|
|
|
// the transaction isn't seen within the network before the passed timeout,
|
2016-11-22 06:08:44 +03:00
|
|
|
// then an error is returned.
|
|
|
|
// TODO(roasbeef): add another method which creates queue of all seen transactions
|
2017-01-06 00:56:27 +03:00
|
|
|
func (n *networkHarness) WaitForTxBroadcast(ctx context.Context, txid chainhash.Hash) error {
|
2016-08-31 05:34:13 +03:00
|
|
|
eventChan := make(chan struct{})
|
|
|
|
|
2017-03-14 08:15:41 +03:00
|
|
|
n.bitcoinWatchRequests <- &txWatchRequest{
|
|
|
|
txid: txid,
|
|
|
|
eventChan: eventChan,
|
|
|
|
}
|
2016-08-31 05:34:13 +03:00
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
select {
|
|
|
|
case <-eventChan:
|
|
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fmt.Errorf("tx not seen before context timeout")
|
|
|
|
}
|
2016-08-31 05:34:13 +03:00
|
|
|
}
|
|
|
|
|
2016-10-15 16:18:38 +03:00
|
|
|
// OpenChannel attempts to open a channel between srcNode and destNode with the
|
2016-09-18 03:34:39 +03:00
|
|
|
// passed channel funding parameters. If the passed context has a timeout, then
|
2016-10-15 16:18:38 +03:00
|
|
|
// if the timeout is reached before the channel pending notification is
|
2016-09-18 03:34:39 +03:00
|
|
|
// received, an error is returned.
|
2016-08-31 21:59:08 +03:00
|
|
|
func (n *networkHarness) OpenChannel(ctx context.Context,
|
2016-09-10 23:14:28 +03:00
|
|
|
srcNode, destNode *lightningNode, amt btcutil.Amount,
|
2017-01-10 06:34:22 +03:00
|
|
|
pushAmt btcutil.Amount, numConfs uint32) (lnrpc.Lightning_OpenChannelClient, error) {
|
2016-08-31 21:59:08 +03:00
|
|
|
|
|
|
|
openReq := &lnrpc.OpenChannelRequest{
|
2016-10-28 05:43:31 +03:00
|
|
|
NodePubkey: destNode.PubKey[:],
|
2016-09-14 01:38:37 +03:00
|
|
|
LocalFundingAmount: int64(amt),
|
2017-01-10 06:34:22 +03:00
|
|
|
PushSat: int64(pushAmt),
|
2016-09-14 01:38:37 +03:00
|
|
|
NumConfs: numConfs,
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
2016-09-15 21:59:51 +03:00
|
|
|
|
2016-08-31 21:59:08 +03:00
|
|
|
respStream, err := srcNode.OpenChannel(ctx, openReq)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to open channel between "+
|
|
|
|
"alice and bob: %v", err)
|
|
|
|
}
|
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
chanOpen := make(chan struct{})
|
|
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
|
|
// Consume the "channel pending" update. This waits until the node
|
|
|
|
// notifies us that the final message in the channel funding workflow
|
|
|
|
// has been sent to the remote node.
|
|
|
|
resp, err := respStream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
errChan <- err
|
2016-09-26 20:52:25 +03:00
|
|
|
return
|
2016-09-18 03:34:39 +03:00
|
|
|
}
|
|
|
|
if _, ok := resp.Update.(*lnrpc.OpenStatusUpdate_ChanPending); !ok {
|
|
|
|
errChan <- fmt.Errorf("expected channel pending update, "+
|
|
|
|
"instead got %v", resp)
|
2016-09-26 20:52:25 +03:00
|
|
|
return
|
2016-09-18 03:34:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
close(chanOpen)
|
|
|
|
}()
|
2016-08-31 21:59:08 +03:00
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2017-01-24 05:19:54 +03:00
|
|
|
return nil, fmt.Errorf("timeout reached before chan pending "+
|
|
|
|
"update sent: %v", err)
|
2016-09-18 03:34:39 +03:00
|
|
|
case err := <-errChan:
|
|
|
|
return nil, err
|
|
|
|
case <-chanOpen:
|
|
|
|
return respStream, nil
|
|
|
|
}
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
2017-01-31 07:21:52 +03:00
|
|
|
// OpenPendingChannel attempts to open a channel between srcNode and destNode with the
|
|
|
|
// passed channel funding parameters. If the passed context has a timeout, then
|
|
|
|
// if the timeout is reached before the channel pending notification is
|
|
|
|
// received, an error is returned.
|
|
|
|
func (n *networkHarness) OpenPendingChannel(ctx context.Context,
|
|
|
|
srcNode, destNode *lightningNode, amt btcutil.Amount,
|
|
|
|
pushAmt btcutil.Amount, numConfs uint32) (*lnrpc.PendingUpdate, error) {
|
|
|
|
|
|
|
|
openReq := &lnrpc.OpenChannelRequest{
|
|
|
|
NodePubkey: destNode.PubKey[:],
|
|
|
|
LocalFundingAmount: int64(amt),
|
|
|
|
PushSat: int64(pushAmt),
|
|
|
|
NumConfs: numConfs,
|
|
|
|
}
|
|
|
|
|
|
|
|
respStream, err := srcNode.OpenChannel(ctx, openReq)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to open channel between "+
|
|
|
|
"alice and bob: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
chanPending := make(chan *lnrpc.PendingUpdate)
|
|
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
|
|
// Consume the "channel pending" update. This waits until the node
|
|
|
|
// notifies us that the final message in the channel funding workflow
|
|
|
|
// has been sent to the remote node.
|
|
|
|
resp, err := respStream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pendingResp, ok := resp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
|
|
|
if !ok {
|
|
|
|
errChan <- fmt.Errorf("expected channel pending update, "+
|
|
|
|
"instead got %v", resp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
chanPending <- pendingResp.ChanPending
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil, fmt.Errorf("timeout reached before chan pending " +
|
|
|
|
"update sent")
|
|
|
|
case err := <-errChan:
|
|
|
|
return nil, err
|
|
|
|
case pendingChan := <-chanPending:
|
|
|
|
return pendingChan, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-31 21:59:08 +03:00
|
|
|
// WaitForChannelOpen waits for a notification that a channel is open by
|
2016-09-18 03:34:39 +03:00
|
|
|
// consuming a message from the past open channel stream. If the passed context
|
|
|
|
// has a timeout, then if the timeout is reached before the channel has been
|
|
|
|
// opened, then an error is returned.
|
|
|
|
func (n *networkHarness) WaitForChannelOpen(ctx context.Context,
|
|
|
|
openChanStream lnrpc.Lightning_OpenChannelClient) (*lnrpc.ChannelPoint, error) {
|
|
|
|
|
|
|
|
errChan := make(chan error)
|
|
|
|
respChan := make(chan *lnrpc.ChannelPoint)
|
|
|
|
go func() {
|
|
|
|
resp, err := openChanStream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
errChan <- fmt.Errorf("unable to read rpc resp: %v", err)
|
2016-09-26 20:52:25 +03:00
|
|
|
return
|
2016-09-18 03:34:39 +03:00
|
|
|
}
|
|
|
|
fundingResp, ok := resp.Update.(*lnrpc.OpenStatusUpdate_ChanOpen)
|
|
|
|
if !ok {
|
|
|
|
errChan <- fmt.Errorf("expected channel open update, "+
|
|
|
|
"instead got %v", resp)
|
2016-09-26 20:52:25 +03:00
|
|
|
return
|
2016-09-18 03:34:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
respChan <- fundingResp.ChanOpen.ChannelPoint
|
|
|
|
}()
|
2016-08-31 21:59:08 +03:00
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil, fmt.Errorf("timeout reached while waiting for " +
|
|
|
|
"channel open")
|
|
|
|
case err := <-errChan:
|
|
|
|
return nil, err
|
|
|
|
case chanPoint := <-respChan:
|
|
|
|
return chanPoint, nil
|
|
|
|
}
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// CloseChannel close channel attempts to close the channel indicated by the
|
2016-09-18 03:34:39 +03:00
|
|
|
// passed channel point, initiated by the passed lnNode. If the passed context
|
|
|
|
// has a timeout, then if the timeout is reached before the channel close is
|
|
|
|
// pending, then an error is returned.
|
2016-08-31 21:59:08 +03:00
|
|
|
func (n *networkHarness) CloseChannel(ctx context.Context,
|
2016-09-10 23:14:28 +03:00
|
|
|
lnNode *lightningNode, cp *lnrpc.ChannelPoint,
|
2017-01-06 00:56:27 +03:00
|
|
|
force bool) (lnrpc.Lightning_CloseChannelClient, *chainhash.Hash, error) {
|
2016-08-31 21:59:08 +03:00
|
|
|
|
|
|
|
closeReq := &lnrpc.CloseChannelRequest{
|
2016-09-12 22:33:22 +03:00
|
|
|
ChannelPoint: cp,
|
|
|
|
Force: force,
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
closeRespStream, err := lnNode.CloseChannel(ctx, closeReq)
|
|
|
|
if err != nil {
|
2016-12-14 02:32:44 +03:00
|
|
|
return nil, nil, fmt.Errorf("unable to close channel: %v", err)
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
errChan := make(chan error)
|
2017-01-06 00:56:27 +03:00
|
|
|
fin := make(chan *chainhash.Hash)
|
2016-09-18 03:34:39 +03:00
|
|
|
go func() {
|
|
|
|
// Consume the "channel close" update in order to wait for the closing
|
|
|
|
// transaction to be broadcast, then wait for the closing tx to be seen
|
|
|
|
// within the network.
|
|
|
|
closeResp, err := closeRespStream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pendingClose, ok := closeResp.Update.(*lnrpc.CloseStatusUpdate_ClosePending)
|
|
|
|
if !ok {
|
|
|
|
errChan <- fmt.Errorf("expected channel close update, "+
|
|
|
|
"instead got %v", pendingClose)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-06 00:56:27 +03:00
|
|
|
closeTxid, err := chainhash.NewHash(pendingClose.ClosePending.Txid)
|
2016-09-18 03:34:39 +03:00
|
|
|
if err != nil {
|
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := n.WaitForTxBroadcast(ctx, *closeTxid); err != nil {
|
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
2016-12-14 02:32:44 +03:00
|
|
|
fin <- closeTxid
|
2016-09-18 03:34:39 +03:00
|
|
|
}()
|
|
|
|
|
|
|
|
// Wait until either the deadline for the context expires, an error
|
|
|
|
// occurs, or the channel close update is received.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2016-12-14 02:32:44 +03:00
|
|
|
return nil, nil, fmt.Errorf("timeout reached before channel close " +
|
2016-09-18 03:34:39 +03:00
|
|
|
"initiated")
|
|
|
|
case err := <-errChan:
|
2016-12-14 02:32:44 +03:00
|
|
|
return nil, nil, err
|
|
|
|
case closeTxid := <-fin:
|
|
|
|
return closeRespStream, closeTxid, nil
|
2016-09-14 04:56:35 +03:00
|
|
|
}
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// WaitForChannelClose waits for a notification from the passed channel close
|
2016-09-18 03:34:39 +03:00
|
|
|
// stream that the node has deemed the channel has been fully closed. If the
|
|
|
|
// passed context has a timeout, then if the timeout is reached before the
|
|
|
|
// notification is received then an error is returned.
|
|
|
|
func (n *networkHarness) WaitForChannelClose(ctx context.Context,
|
2017-01-06 00:56:27 +03:00
|
|
|
closeChanStream lnrpc.Lightning_CloseChannelClient) (*chainhash.Hash, error) {
|
2016-09-18 03:34:39 +03:00
|
|
|
|
|
|
|
errChan := make(chan error)
|
|
|
|
updateChan := make(chan *lnrpc.CloseStatusUpdate_ChanClose)
|
|
|
|
go func() {
|
|
|
|
closeResp, err := closeChanStream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
errChan <- err
|
|
|
|
return
|
|
|
|
}
|
2016-08-31 21:59:08 +03:00
|
|
|
|
2016-09-18 03:34:39 +03:00
|
|
|
closeFin, ok := closeResp.Update.(*lnrpc.CloseStatusUpdate_ChanClose)
|
|
|
|
if !ok {
|
|
|
|
errChan <- fmt.Errorf("expected channel close update, "+
|
|
|
|
"instead got %v", closeFin)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
updateChan <- closeFin
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Wait until either the deadline for the context expires, an error
|
|
|
|
// occurs, or the channel close update is received.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil, fmt.Errorf("timeout reached before update sent")
|
|
|
|
case err := <-errChan:
|
|
|
|
return nil, err
|
|
|
|
case update := <-updateChan:
|
2017-01-06 00:56:27 +03:00
|
|
|
return chainhash.NewHash(update.ChanClose.ClosingTxid)
|
2016-09-18 03:34:39 +03:00
|
|
|
}
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// AssertChannelExists asserts that an active channel identified by
|
2016-09-06 22:04:08 +03:00
|
|
|
// channelPoint is known to exist from the point-of-view of node..
|
|
|
|
func (n *networkHarness) AssertChannelExists(ctx context.Context,
|
2016-09-10 23:14:28 +03:00
|
|
|
node *lightningNode, chanPoint *wire.OutPoint) error {
|
2016-08-31 21:59:08 +03:00
|
|
|
|
2016-09-26 20:31:07 +03:00
|
|
|
req := &lnrpc.ListChannelsRequest{}
|
|
|
|
resp, err := node.ListChannels(ctx, req)
|
2016-08-31 21:59:08 +03:00
|
|
|
if err != nil {
|
2016-09-26 20:31:07 +03:00
|
|
|
return fmt.Errorf("unable fetch node's channels: %v", err)
|
2016-08-31 21:59:08 +03:00
|
|
|
}
|
|
|
|
|
2016-09-26 20:31:07 +03:00
|
|
|
for _, channel := range resp.Channels {
|
|
|
|
if channel.ChannelPoint == chanPoint.String() {
|
|
|
|
return nil
|
2016-09-06 22:04:08 +03:00
|
|
|
}
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2016-09-06 22:04:08 +03:00
|
|
|
return fmt.Errorf("channel not found")
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2016-09-10 23:14:28 +03:00
|
|
|
// DumpLogs reads the current logs generated by the passed node, and returns
|
|
|
|
// the logs as a single string. This function is useful for examining the logs
|
|
|
|
// of a particular node in the case of a test failure.
|
2016-09-15 21:59:51 +03:00
|
|
|
// Logs from lightning node being generated with delay - you should
|
|
|
|
// add time.Sleep() in order to get all logs.
|
2016-09-10 23:14:28 +03:00
|
|
|
func (n *networkHarness) DumpLogs(node *lightningNode) (string, error) {
|
|
|
|
logFile := fmt.Sprintf("%v/simnet/lnd.log", node.cfg.LogDir)
|
|
|
|
|
2016-09-15 21:59:51 +03:00
|
|
|
buf, err := ioutil.ReadFile(logFile)
|
2016-08-30 07:55:01 +03:00
|
|
|
if err != nil {
|
2016-09-10 23:14:28 +03:00
|
|
|
return "", err
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
|
|
|
|
2016-09-15 21:59:51 +03:00
|
|
|
return string(buf), nil
|
2016-08-30 07:55:01 +03:00
|
|
|
}
|
2016-09-26 20:31:07 +03:00
|
|
|
|
2017-01-12 01:21:04 +03:00
|
|
|
// SendCoins attempts to send amt satoshis from the internal mining node to the
|
|
|
|
// targeted lightning node.
|
2016-09-26 20:31:07 +03:00
|
|
|
func (n *networkHarness) SendCoins(ctx context.Context, amt btcutil.Amount,
|
|
|
|
target *lightningNode) error {
|
|
|
|
|
2016-09-26 21:54:13 +03:00
|
|
|
balReq := &lnrpc.WalletBalanceRequest{}
|
|
|
|
initialBalance, err := target.WalletBalance(ctx, balReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-26 20:31:07 +03:00
|
|
|
// First, obtain an address from the target lightning node, preferring
|
|
|
|
// to receive a p2wkh address s.t the output can immediately be used as
|
|
|
|
// an input to a funding transaction.
|
|
|
|
addrReq := &lnrpc.NewAddressRequest{
|
|
|
|
Type: lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH,
|
|
|
|
}
|
|
|
|
resp, err := target.NewAddress(ctx, addrReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addr, err := btcutil.DecodeAddress(resp.Address, n.netParams)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addrScript, err := txscript.PayToAddrScript(addr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a transaction which creates an output to the target
|
|
|
|
// pkScript of the desired amount.
|
|
|
|
output := &wire.TxOut{
|
|
|
|
PkScript: addrScript,
|
|
|
|
Value: int64(amt),
|
|
|
|
}
|
2017-01-06 00:56:27 +03:00
|
|
|
if _, err := n.Miner.SendOutputs([]*wire.TxOut{output}, 30); err != nil {
|
2016-09-26 20:31:07 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, generate 6 new blocks to ensure the output gains a
|
|
|
|
// sufficient number of confirmations.
|
|
|
|
if _, err := n.Miner.Node.Generate(6); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-26 21:54:13 +03:00
|
|
|
// Pause until the nodes current wallet balances reflects the amount
|
|
|
|
// sent to it above.
|
|
|
|
// TODO(roasbeef): factor out into helper func
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-time.Tick(time.Millisecond * 50):
|
|
|
|
currentBal, err := target.WalletBalance(ctx, balReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if currentBal.Balance == initialBalance.Balance+amt.ToBTC() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
case <-time.After(time.Second * 30):
|
|
|
|
return fmt.Errorf("balances not synced after deadline")
|
|
|
|
}
|
|
|
|
}
|
2016-09-26 20:31:07 +03:00
|
|
|
}
|