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 }