github.com/decred/dcrlnd@v0.7.6/internal/testutils/remotewallet.go (about)

     1  package testutils
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	pb "decred.org/dcrwallet/v4/rpc/walletrpc"
    16  	"github.com/decred/dcrd/rpcclient/v8"
    17  	"github.com/decred/dcrlnd/lntest/wait"
    18  	"google.golang.org/grpc"
    19  	"google.golang.org/grpc/backoff"
    20  	"google.golang.org/grpc/credentials"
    21  )
    22  
    23  var activeNodes int32
    24  
    25  type rpcSyncer struct {
    26  	c pb.WalletLoaderService_RpcSyncClient
    27  }
    28  
    29  func (r *rpcSyncer) RecvSynced() (bool, error) {
    30  	msg, err := r.c.Recv()
    31  	if err != nil {
    32  		// All errors are final here.
    33  		return false, err
    34  	}
    35  	return msg.Synced, nil
    36  }
    37  
    38  type spvSyncer struct {
    39  	c pb.WalletLoaderService_SpvSyncClient
    40  }
    41  
    42  func (r *spvSyncer) RecvSynced() (bool, error) {
    43  	msg, err := r.c.Recv()
    44  	if err != nil {
    45  		// All errors are final here.
    46  		return false, err
    47  	}
    48  	return msg.Synced, nil
    49  }
    50  
    51  type syncer interface {
    52  	RecvSynced() (bool, error)
    53  }
    54  
    55  func consumeSyncMsgs(syncStream syncer, onSyncedChan chan struct{}) {
    56  	for {
    57  		synced, err := syncStream.RecvSynced()
    58  		if err != nil {
    59  			// All errors are final here.
    60  			return
    61  		}
    62  		if synced {
    63  			onSyncedChan <- struct{}{}
    64  			return
    65  		}
    66  	}
    67  }
    68  
    69  func tlsCertFromFile(fname string) (*x509.CertPool, error) {
    70  	b, err := ioutil.ReadFile(fname)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	cp := x509.NewCertPool()
    75  	if !cp.AppendCertsFromPEM(b) {
    76  		return nil, fmt.Errorf("credentials: failed to append certificates")
    77  	}
    78  
    79  	return cp, nil
    80  }
    81  
    82  type SPVConfig struct {
    83  	Address string
    84  }
    85  
    86  // NewCustomTestRemoteDcrwallet runs a dcrwallet instance for use during tests.
    87  func NewCustomTestRemoteDcrwallet(t TB, nodeName, dataDir string,
    88  	hdSeed, privatePass []byte,
    89  	dcrd *rpcclient.ConnConfig, spv *SPVConfig) (*grpc.ClientConn, func()) {
    90  
    91  	if dcrd == nil && spv == nil {
    92  		t.Fatalf("either dcrd or spv config needs to be specified")
    93  	}
    94  	if dcrd != nil && spv != nil {
    95  		t.Fatalf("only one of dcrd or spv config needs to be specified")
    96  	}
    97  
    98  	tlsCertPath := path.Join(dataDir, "rpc.cert")
    99  	tlsKeyPath := path.Join(dataDir, "rpc.key")
   100  
   101  	pipeTX, err := newIPCPipePair(true, false)
   102  	if err != nil {
   103  		t.Fatalf("unable to create pipe for dcrd IPC: %v", err)
   104  	}
   105  	pipeRX, err := newIPCPipePair(false, true)
   106  	if err != nil {
   107  		t.Fatalf("unable to create pipe for dcrd IPC: %v", err)
   108  	}
   109  
   110  	// Setup the args to run the underlying dcrwallet.
   111  	id := atomic.AddInt32(&activeNodes, 1)
   112  	args := []string{
   113  		"--noinitialload",
   114  		"--debuglevel=debug",
   115  		"--simnet",
   116  		"--nolegacyrpc",
   117  		"--grpclisten=127.0.0.1:0",
   118  		"--appdata=" + dataDir,
   119  		"--tlscurve=P-256",
   120  		"--rpccert=" + tlsCertPath,
   121  		"--rpckey=" + tlsKeyPath,
   122  		"--clientcafile=" + tlsCertPath,
   123  		"--rpclistenerevents",
   124  	}
   125  	args = appendOSWalletArgs(&pipeTX, &pipeRX, args)
   126  
   127  	logFilePath := path.Join(fmt.Sprintf("output-remotedcrw-%.2d-%s.log",
   128  		id, nodeName))
   129  	logFile, err := os.Create(logFilePath)
   130  	if err != nil {
   131  		t.Logf("Wallet dir: %s", dataDir)
   132  		t.Fatalf("Unable to create %s dcrwallet log file: %v",
   133  			nodeName, err)
   134  	}
   135  
   136  	const dcrwalletExe = "dcrwallet-dcrlnd"
   137  
   138  	// Run dcrwallet.
   139  	cmd := exec.Command(dcrwalletExe, args...)
   140  	cmd.Stdout = logFile
   141  	cmd.Stderr = logFile
   142  	setOSWalletCmdOptions(&pipeTX, &pipeRX, cmd)
   143  	err = cmd.Start()
   144  	if err != nil {
   145  		t.Logf("Wallet dir: %s", dataDir)
   146  		t.Fatalf("Unable to start %s dcrwallet: %v", nodeName, err)
   147  	}
   148  
   149  	// Read the subsystem addresses.
   150  	gotSubsysAddrs := make(chan struct{})
   151  	var grpcAddr string
   152  	go func() {
   153  		for grpcAddr == "" {
   154  			msg, err := nextIPCMessage(pipeTX.r)
   155  			if err != nil {
   156  				t.Logf("Unable to read next IPC message: %v", err)
   157  				return
   158  			}
   159  			switch msg := msg.(type) {
   160  			case boundGRPCListenAddrEvent:
   161  				grpcAddr = string(msg)
   162  				close(gotSubsysAddrs)
   163  			}
   164  		}
   165  
   166  		// Drain messages until the pipe is closed.
   167  		var err error
   168  		for err == nil {
   169  			_, err = nextIPCMessage(pipeRX.r)
   170  		}
   171  	}()
   172  
   173  	// Read the wallet TLS cert and client cert and key files.
   174  	var caCert *x509.CertPool
   175  	var clientCert tls.Certificate
   176  	err = wait.NoError(func() error {
   177  		var err error
   178  		caCert, err = tlsCertFromFile(tlsCertPath)
   179  		if err != nil {
   180  			return fmt.Errorf("unable to load wallet ca cert: %v", err)
   181  		}
   182  
   183  		clientCert, err = tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
   184  		if err != nil {
   185  			return fmt.Errorf("unable to load wallet cert and key files: %v", err)
   186  		}
   187  
   188  		return nil
   189  	}, time.Second*30)
   190  	if err != nil {
   191  		t.Logf("Wallet dir: %s", dataDir)
   192  		t.Fatalf("Unable to read ca cert file: %v", err)
   193  	}
   194  
   195  	// Wait until the gRPC address is read via IPC.
   196  	select {
   197  	case <-gotSubsysAddrs:
   198  	case <-time.After(time.Second * 30):
   199  		t.Fatalf("wallet did not send gRPC address through IPC")
   200  	}
   201  
   202  	// Setup the TLS config and credentials.
   203  	tlsCfg := &tls.Config{
   204  		ServerName:   "localhost",
   205  		RootCAs:      caCert,
   206  		Certificates: []tls.Certificate{clientCert},
   207  	}
   208  	creds := credentials.NewTLS(tlsCfg)
   209  
   210  	opts := []grpc.DialOption{
   211  		grpc.WithBlock(),
   212  		grpc.WithTransportCredentials(creds),
   213  		grpc.WithConnectParams(grpc.ConnectParams{
   214  			Backoff: backoff.Config{
   215  				BaseDelay:  time.Millisecond * 20,
   216  				Multiplier: 1,
   217  				Jitter:     0.2,
   218  				MaxDelay:   time.Millisecond * 20,
   219  			},
   220  			MinConnectTimeout: time.Millisecond * 20,
   221  		}),
   222  	}
   223  	ctxb := context.Background()
   224  	ctx, cancel := context.WithTimeout(ctxb, time.Second*30)
   225  	defer cancel()
   226  	conn, err := grpc.DialContext(ctx, grpcAddr, opts...)
   227  	if err != nil {
   228  		t.Logf("Wallet dir: %s", dataDir)
   229  		t.Fatalf("Unable to dial grpc: %v", err)
   230  	}
   231  
   232  	loader := pb.NewWalletLoaderServiceClient(conn)
   233  
   234  	// Create the wallet.
   235  	reqCreate := &pb.CreateWalletRequest{
   236  		Seed:              hdSeed,
   237  		PublicPassphrase:  privatePass,
   238  		PrivatePassphrase: privatePass,
   239  	}
   240  	ctx, cancel = context.WithTimeout(ctxb, time.Second*30)
   241  	defer cancel()
   242  	_, err = loader.CreateWallet(ctx, reqCreate)
   243  	if err != nil {
   244  		t.Logf("Wallet dir: %s", dataDir)
   245  		t.Fatalf("unable to create wallet: %v", err)
   246  	}
   247  
   248  	ctxSync, cancelSync := context.WithCancel(context.Background())
   249  	var syncStream syncer
   250  	if dcrd != nil {
   251  		// Run the rpc syncer.
   252  		req := &pb.RpcSyncRequest{
   253  			NetworkAddress:    dcrd.Host,
   254  			Username:          dcrd.User,
   255  			Password:          []byte(dcrd.Pass),
   256  			Certificate:       dcrd.Certificates,
   257  			DiscoverAccounts:  true,
   258  			PrivatePassphrase: privatePass,
   259  		}
   260  		var res pb.WalletLoaderService_RpcSyncClient
   261  		res, err = loader.RpcSync(ctxSync, req)
   262  		syncStream = &rpcSyncer{c: res}
   263  	} else if spv != nil {
   264  		// Run the spv syncer.
   265  		req := &pb.SpvSyncRequest{
   266  			SpvConnect:        []string{spv.Address},
   267  			DiscoverAccounts:  true,
   268  			PrivatePassphrase: privatePass,
   269  		}
   270  		var res pb.WalletLoaderService_SpvSyncClient
   271  		res, err = loader.SpvSync(ctxSync, req)
   272  		syncStream = &spvSyncer{c: res}
   273  	}
   274  	if err != nil {
   275  		cancelSync()
   276  		t.Fatalf("error running rpc sync: %v", err)
   277  	}
   278  
   279  	// Wait for the wallet to sync. Remote wallets are assumed synced
   280  	// before an ln wallet is started for them.
   281  	onSyncedChan := make(chan struct{})
   282  	go consumeSyncMsgs(syncStream, onSyncedChan)
   283  	select {
   284  	case <-onSyncedChan:
   285  		// Sync done.
   286  	case <-time.After(time.Second * 60):
   287  		cancelSync()
   288  		t.Fatalf("timeout waiting for initial sync to complete")
   289  	}
   290  
   291  	cleanup := func() {
   292  		cancelSync()
   293  
   294  		if cmd.ProcessState != nil {
   295  			return
   296  		}
   297  
   298  		if t.Failed() {
   299  			t.Logf("Wallet data at %s", dataDir)
   300  		}
   301  
   302  		err := cmd.Process.Signal(os.Interrupt)
   303  		if err != nil {
   304  			t.Errorf("Error sending SIGINT to %s dcrwallet: %v",
   305  				nodeName, err)
   306  			return
   307  		}
   308  
   309  		// Wait for dcrwallet to exit or force kill it after a timeout.
   310  		// For this, we run the wait on a goroutine and signal once it
   311  		// has returned.
   312  		errChan := make(chan error)
   313  		go func() {
   314  			errChan <- cmd.Wait()
   315  		}()
   316  
   317  		select {
   318  		case err := <-errChan:
   319  			if err != nil {
   320  				t.Errorf("%s dcrwallet exited with an error: %v",
   321  					nodeName, err)
   322  			}
   323  
   324  		case <-time.After(time.Second * 15):
   325  			t.Errorf("%s dcrwallet timed out after SIGINT", nodeName)
   326  			err := cmd.Process.Kill()
   327  			if err != nil {
   328  				t.Errorf("Error killing %s dcrwallet: %v",
   329  					nodeName, err)
   330  			}
   331  		}
   332  
   333  		pipeTX.close()
   334  		pipeRX.close()
   335  	}
   336  
   337  	return conn, cleanup
   338  }
   339  
   340  // NewRPCSyncingTestRemoteDcrwallet creates a new dcrwallet process that can be
   341  // used by a remotedcrwallet instance to perform the interface tests. This
   342  // remote wallet syncs to the passed dcrd node using RPC mode sycing.
   343  //
   344  // This function returns the grpc conn and a cleanup function to close the
   345  // wallet.
   346  func NewRPCSyncingTestRemoteDcrwallet(t TB, dcrd *rpcclient.ConnConfig) (*grpc.ClientConn, func()) {
   347  	tempDir, err := ioutil.TempDir("", "test-dcrw-rpc")
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  
   352  	var seed [32]byte
   353  	c, tearDownWallet := NewCustomTestRemoteDcrwallet(t, "remotedcrw", tempDir,
   354  		seed[:], []byte("pass"), dcrd, nil)
   355  	tearDown := func() {
   356  		tearDownWallet()
   357  
   358  		if !t.Failed() {
   359  			os.RemoveAll(tempDir)
   360  		}
   361  	}
   362  
   363  	return c, tearDown
   364  }
   365  
   366  // NewRPCSyncingTestRemoteDcrwallet creates a new dcrwallet process that can be
   367  // used by a remotedcrwallet instance to perform the interface tests. This
   368  // remote wallet syncs to the passed dcrd node using SPV mode sycing.
   369  //
   370  // This function returns the grpc conn and a cleanup function to close the
   371  // wallet.
   372  func NewSPVSyncingTestRemoteDcrwallet(t TB, p2pAddr string) (*grpc.ClientConn, func()) {
   373  	tempDir, err := ioutil.TempDir("", "test-dcrw-spv")
   374  	if err != nil {
   375  		t.Fatal(err)
   376  	}
   377  
   378  	var seed [32]byte
   379  	c, tearDownWallet := NewCustomTestRemoteDcrwallet(t, "remotedcrw", tempDir,
   380  		seed[:], []byte("pass"), nil, &SPVConfig{Address: p2pAddr})
   381  	tearDown := func() {
   382  		tearDownWallet()
   383  
   384  		if !t.Failed() {
   385  			os.RemoveAll(tempDir)
   386  		}
   387  	}
   388  
   389  	return c, tearDown
   390  }
   391  
   392  // SetPerAccountPassphrase calls the SetAccountPassphrase rpc endpoint on the
   393  // wallet at the given conn, setting it to the specified passphrse.
   394  //
   395  // This function expects a conn returned by NewCustomTestRemoteDcrwallet.
   396  func SetPerAccountPassphrase(conn *grpc.ClientConn, passphrase []byte) error {
   397  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
   398  	defer cancel()
   399  
   400  	// Set the wallet to use per-account passphrases.
   401  	wallet := pb.NewWalletServiceClient(conn)
   402  	reqSetAcctPwd := &pb.SetAccountPassphraseRequest{
   403  		AccountNumber:        0,
   404  		WalletPassphrase:     passphrase,
   405  		NewAccountPassphrase: passphrase,
   406  	}
   407  	_, err := wallet.SetAccountPassphrase(ctx, reqSetAcctPwd)
   408  	return err
   409  }