decred.org/dcrwallet/v3@v3.1.0/chain/sync.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package chain 6 7 import ( 8 "context" 9 "crypto/tls" 10 "crypto/x509" 11 "encoding/json" 12 "net" 13 "runtime/trace" 14 "sync" 15 "sync/atomic" 16 17 "decred.org/dcrwallet/v3/errors" 18 "decred.org/dcrwallet/v3/rpc/client/dcrd" 19 "decred.org/dcrwallet/v3/validate" 20 "decred.org/dcrwallet/v3/wallet" 21 "github.com/decred/dcrd/blockchain/stake/v5" 22 "github.com/decred/dcrd/chaincfg/chainhash" 23 "github.com/decred/dcrd/wire" 24 "github.com/jrick/wsrpc/v2" 25 "golang.org/x/sync/errgroup" 26 ) 27 28 var requiredAPIVersion = semver{Major: 8, Minor: 0, Patch: 0} 29 30 // Syncer implements wallet synchronization services by processing 31 // notifications from a dcrd JSON-RPC server. 32 type Syncer struct { 33 atomicWalletSynced uint32 // CAS (synced=1) when wallet syncing complete 34 35 wallet *wallet.Wallet 36 opts *RPCOptions 37 rpc *dcrd.RPC 38 notifier *notifier 39 40 discoverAccts bool 41 mu sync.Mutex 42 43 // Sidechain management 44 sidechains wallet.SidechainForest 45 sidechainsMu sync.Mutex 46 relevantTxs map[chainhash.Hash][]*wire.MsgTx 47 48 cb *Callbacks 49 } 50 51 // RPCOptions specifies the network and security settings for establishing a 52 // websocket connection to a dcrd JSON-RPC server. 53 type RPCOptions struct { 54 Address string 55 DefaultPort string 56 User string 57 Pass string 58 Dial func(ctx context.Context, network, address string) (net.Conn, error) 59 CA []byte 60 Insecure bool 61 } 62 63 // NewSyncer creates a Syncer that will sync the wallet using dcrd JSON-RPC. 64 func NewSyncer(w *wallet.Wallet, r *RPCOptions) *Syncer { 65 return &Syncer{ 66 wallet: w, 67 opts: r, 68 discoverAccts: !w.Locked(), 69 relevantTxs: make(map[chainhash.Hash][]*wire.MsgTx), 70 } 71 } 72 73 // Callbacks contains optional callback functions to notify events during 74 // the syncing process. All callbacks are called synchronously and block the 75 // syncer from continuing. 76 type Callbacks struct { 77 Synced func(synced bool) 78 FetchMissingCFiltersStarted func() 79 FetchMissingCFiltersProgress func(startCFiltersHeight, endCFiltersHeight int32) 80 FetchMissingCFiltersFinished func() 81 FetchHeadersStarted func() 82 FetchHeadersProgress func(lastHeaderHeight int32, lastHeaderTime int64) 83 FetchHeadersFinished func() 84 DiscoverAddressesStarted func() 85 DiscoverAddressesFinished func() 86 RescanStarted func() 87 RescanProgress func(rescannedThrough int32) 88 RescanFinished func() 89 } 90 91 // SetCallbacks sets the possible various callbacks that are used 92 // to notify interested parties to the syncing progress. 93 func (s *Syncer) SetCallbacks(cb *Callbacks) { 94 s.cb = cb 95 } 96 97 // DisableDiscoverAccounts disables account discovery. This has an effect only 98 // if called before the main Run() executes the account discovery process. 99 func (s *Syncer) DisableDiscoverAccounts() { 100 s.mu.Lock() 101 s.discoverAccts = false 102 s.mu.Unlock() 103 } 104 105 // synced checks the atomic that controls wallet syncness and if previously 106 // unsynced, updates to synced and notifies the callback, if set. 107 func (s *Syncer) synced() { 108 swapped := atomic.CompareAndSwapUint32(&s.atomicWalletSynced, 0, 1) 109 if swapped && s.cb != nil && s.cb.Synced != nil { 110 s.cb.Synced(true) 111 } 112 } 113 114 func (s *Syncer) fetchMissingCfiltersStart() { 115 if s.cb != nil && s.cb.FetchMissingCFiltersStarted != nil { 116 s.cb.FetchMissingCFiltersStarted() 117 } 118 } 119 120 func (s *Syncer) fetchMissingCfiltersProgress(startMissingCFilterHeight, endMissinCFilterHeight int32) { 121 if s.cb != nil && s.cb.FetchMissingCFiltersProgress != nil { 122 s.cb.FetchMissingCFiltersProgress(startMissingCFilterHeight, endMissinCFilterHeight) 123 } 124 } 125 126 func (s *Syncer) fetchMissingCfiltersFinished() { 127 if s.cb != nil && s.cb.FetchMissingCFiltersFinished != nil { 128 s.cb.FetchMissingCFiltersFinished() 129 } 130 } 131 132 func (s *Syncer) fetchHeadersStart() { 133 if s.cb != nil && s.cb.FetchHeadersStarted != nil { 134 s.cb.FetchHeadersStarted() 135 } 136 } 137 138 func (s *Syncer) fetchHeadersProgress(fetchedHeadersCount int32, lastHeaderTime int64) { 139 if s.cb != nil && s.cb.FetchHeadersProgress != nil { 140 s.cb.FetchHeadersProgress(fetchedHeadersCount, lastHeaderTime) 141 } 142 } 143 144 func (s *Syncer) fetchHeadersFinished() { 145 if s.cb != nil && s.cb.FetchHeadersFinished != nil { 146 s.cb.FetchHeadersFinished() 147 } 148 } 149 func (s *Syncer) discoverAddressesStart() { 150 if s.cb != nil && s.cb.DiscoverAddressesStarted != nil { 151 s.cb.DiscoverAddressesStarted() 152 } 153 } 154 155 func (s *Syncer) discoverAddressesFinished() { 156 if s.cb != nil && s.cb.DiscoverAddressesFinished != nil { 157 s.cb.DiscoverAddressesFinished() 158 } 159 } 160 161 func (s *Syncer) rescanStart() { 162 if s.cb != nil && s.cb.RescanStarted != nil { 163 s.cb.RescanStarted() 164 } 165 } 166 167 func (s *Syncer) rescanProgress(rescannedThrough int32) { 168 if s.cb != nil && s.cb.RescanProgress != nil { 169 s.cb.RescanProgress(rescannedThrough) 170 } 171 } 172 173 func (s *Syncer) rescanFinished() { 174 if s.cb != nil && s.cb.RescanFinished != nil { 175 s.cb.RescanFinished() 176 } 177 } 178 179 func normalizeAddress(addr string, defaultPort string) (hostport string, err error) { 180 host, port, origErr := net.SplitHostPort(addr) 181 if origErr == nil { 182 return net.JoinHostPort(host, port), nil 183 } 184 addr = net.JoinHostPort(addr, defaultPort) 185 _, _, err = net.SplitHostPort(addr) 186 if err != nil { 187 return "", origErr 188 } 189 return addr, nil 190 } 191 192 // hashStop is a zero value stop hash for fetching all possible data using 193 // locators. 194 var hashStop chainhash.Hash 195 196 // Run synchronizes the wallet, returning when synchronization fails or the 197 // context is cancelled. If startupSync is true, all synchronization tasks 198 // needed to fully register the wallet for notifications and synchronize it with 199 // the dcrd server are performed. Otherwise, it will listen for notifications 200 // but not register for any updates. 201 func (s *Syncer) Run(ctx context.Context) (err error) { 202 defer func() { 203 if err != nil { 204 const op errors.Op = "rpcsyncer.Run" 205 err = errors.E(op, err) 206 } 207 }() 208 209 params := s.wallet.ChainParams() 210 211 s.notifier = ¬ifier{ 212 syncer: s, 213 ctx: ctx, 214 closed: make(chan struct{}), 215 } 216 addr, err := normalizeAddress(s.opts.Address, s.opts.DefaultPort) 217 if err != nil { 218 return errors.E(errors.Invalid, err) 219 } 220 if s.opts.Insecure { 221 addr = "ws://" + addr + "/ws" 222 } else { 223 addr = "wss://" + addr + "/ws" 224 } 225 opts := make([]wsrpc.Option, 0, 5) 226 opts = append(opts, wsrpc.WithBasicAuth(s.opts.User, s.opts.Pass)) 227 opts = append(opts, wsrpc.WithNotifier(s.notifier)) 228 opts = append(opts, wsrpc.WithoutPongDeadline()) 229 if s.opts.Dial != nil { 230 opts = append(opts, wsrpc.WithDial(s.opts.Dial)) 231 } 232 if len(s.opts.CA) != 0 && !s.opts.Insecure { 233 pool := x509.NewCertPool() 234 pool.AppendCertsFromPEM(s.opts.CA) 235 tc := &tls.Config{ 236 MinVersion: tls.VersionTLS12, 237 CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, 238 CipherSuites: []uint16{ // Only applies to TLS 1.2. TLS 1.3 ciphersuites are not configurable. 239 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 240 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 241 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 242 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 243 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 244 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 245 }, 246 RootCAs: pool, 247 } 248 opts = append(opts, wsrpc.WithTLSConfig(tc)) 249 } 250 client, err := wsrpc.Dial(ctx, addr, opts...) 251 if err != nil { 252 return err 253 } 254 defer client.Close() 255 s.rpc = dcrd.New(client) 256 257 // Verify that the server is running on the expected network. 258 var netID wire.CurrencyNet 259 err = s.rpc.Call(ctx, "getcurrentnet", &netID) 260 if err != nil { 261 return err 262 } 263 if netID != params.Net { 264 return errors.E("mismatched networks") 265 } 266 267 // Ensure the RPC server has a compatible API version. 268 var api struct { 269 Version semver `json:"dcrdjsonrpcapi"` 270 } 271 err = s.rpc.Call(ctx, "version", &api) 272 if err != nil { 273 return err 274 } 275 if !semverCompatible(requiredAPIVersion, api.Version) { 276 return errors.Errorf("advertised API version %v incompatible "+ 277 "with required version %v", api.Version, requiredAPIVersion) 278 } 279 280 // Associate the RPC client with the wallet and remove the association on return. 281 s.wallet.SetNetworkBackend(s.rpc) 282 defer s.wallet.SetNetworkBackend(nil) 283 284 tipHash, tipHeight := s.wallet.MainChainTip(ctx) 285 rescanPoint, err := s.wallet.RescanPoint(ctx) 286 if err != nil { 287 return err 288 } 289 log.Infof("Headers synced through block %v height %d", &tipHash, tipHeight) 290 if rescanPoint != nil { 291 h, err := s.wallet.BlockHeader(ctx, rescanPoint) 292 if err != nil { 293 return err 294 } 295 // The rescan point is the first block that does not have synced 296 // transactions, so we are synced with the parent. 297 log.Infof("Transactions synced through block %v height %d", &h.PrevBlock, h.Height-1) 298 } else { 299 log.Infof("Transactions synced through block %v height %d", &tipHash, tipHeight) 300 } 301 302 if s.wallet.VotingEnabled() { 303 err = s.rpc.Call(ctx, "notifywinningtickets", nil) 304 if err != nil { 305 return err 306 } 307 vb := s.wallet.VoteBits() 308 log.Infof("Wallet voting enabled: vote bits = %#04x, "+ 309 "extended vote bits = %x", vb.Bits, vb.ExtendedBits) 310 log.Infof("Please ensure your wallet remains unlocked so it may vote") 311 } 312 313 // Fetch any missing main chain compact filters. 314 s.fetchMissingCfiltersStart() 315 progress := make(chan wallet.MissingCFilterProgress, 1) 316 go s.wallet.FetchMissingCFiltersWithProgress(ctx, s.rpc, progress) 317 for p := range progress { 318 if p.Err != nil { 319 return p.Err 320 } 321 s.fetchMissingCfiltersProgress(p.BlockHeightStart, p.BlockHeightEnd) 322 } 323 s.fetchMissingCfiltersFinished() 324 325 // Request notifications for connected and disconnected blocks. 326 err = s.rpc.Call(ctx, "notifyblocks", nil) 327 if err != nil { 328 return err 329 } 330 331 // Populate tspends. 332 tspends, err := s.rpc.GetMempoolTSpends(ctx) 333 if err != nil { 334 return err 335 } 336 for _, v := range tspends { 337 s.wallet.AddTSpend(*v) 338 } 339 log.Tracef("TSpends in mempool: %v", len(tspends)) 340 341 // Request notifications for mempool tspennd arrivals. 342 err = s.rpc.Call(ctx, "notifytspend", nil) 343 if err != nil { 344 return err 345 } 346 347 // Fetch new headers and cfilters from the server. 348 locators, err := s.wallet.BlockLocators(ctx, nil) 349 if err != nil { 350 return err 351 } 352 353 cnet := s.wallet.ChainParams().Net 354 s.fetchHeadersStart() 355 for { 356 if err := ctx.Err(); err != nil { 357 return err 358 } 359 headers, err := s.rpc.Headers(ctx, locators, &hashStop) 360 if err != nil { 361 return err 362 } 363 if len(headers) == 0 { 364 break 365 } 366 367 nodes := make([]*wallet.BlockNode, len(headers)) 368 var g errgroup.Group 369 for i := range headers { 370 i := i 371 g.Go(func() error { 372 header := headers[i] 373 hash := header.BlockHash() 374 filter, proofIndex, proof, err := s.rpc.CFilterV2(ctx, &hash) 375 if err != nil { 376 return err 377 } 378 379 err = validate.CFilterV2HeaderCommitment(cnet, header, 380 filter, proofIndex, proof) 381 if err != nil { 382 return err 383 } 384 385 nodes[i] = wallet.NewBlockNode(header, &hash, filter) 386 if wallet.BadCheckpoint(cnet, &hash, int32(header.Height)) { 387 nodes[i].BadCheckpoint() 388 } 389 return nil 390 }) 391 } 392 err = g.Wait() 393 if err != nil { 394 return err 395 } 396 397 var added int 398 for _, n := range nodes { 399 haveBlock, _, _ := s.wallet.BlockInMainChain(ctx, n.Hash) 400 if haveBlock { 401 continue 402 } 403 s.sidechainsMu.Lock() 404 if s.sidechains.AddBlockNode(n) { 405 added++ 406 } 407 s.sidechainsMu.Unlock() 408 } 409 410 s.fetchHeadersProgress(int32(added), headers[len(headers)-1].Timestamp.Unix()) 411 412 log.Infof("Fetched %d new header(s) ending at height %d from %s", 413 added, nodes[len(nodes)-1].Header.Height, client) 414 415 // Stop fetching headers when no new blocks are returned. 416 // Because getheaders did return located blocks, this indicates 417 // that the server is not as far synced as the wallet. Blocks 418 // the server has not processed are not reorged out of the 419 // wallet at this time, but a reorg will switch to a better 420 // chain later if one is discovered. 421 if added == 0 { 422 break 423 } 424 425 s.sidechainsMu.Lock() 426 bestChain, err := s.wallet.EvaluateBestChain(ctx, &s.sidechains) 427 s.sidechainsMu.Unlock() 428 if err != nil { 429 return err 430 } 431 if len(bestChain) == 0 { 432 continue 433 } 434 435 _, err = s.wallet.ValidateHeaderChainDifficulties(ctx, bestChain, 0) 436 if err != nil { 437 return err 438 } 439 440 s.sidechainsMu.Lock() 441 prevChain, err := s.wallet.ChainSwitch(ctx, &s.sidechains, bestChain, nil) 442 s.sidechainsMu.Unlock() 443 if err != nil { 444 return err 445 } 446 447 if len(prevChain) != 0 { 448 log.Infof("Reorganize from %v to %v (total %d block(s) reorged)", 449 prevChain[len(prevChain)-1].Hash, bestChain[len(bestChain)-1].Hash, len(prevChain)) 450 s.sidechainsMu.Lock() 451 for _, n := range prevChain { 452 s.sidechains.AddBlockNode(n) 453 } 454 s.sidechainsMu.Unlock() 455 } 456 tip := bestChain[len(bestChain)-1] 457 if len(bestChain) == 1 { 458 log.Infof("Connected block %v, height %d", tip.Hash, tip.Header.Height) 459 } else { 460 log.Infof("Connected %d blocks, new tip block %v, height %d, date %v", 461 len(bestChain), tip.Hash, tip.Header.Height, tip.Header.Timestamp) 462 } 463 464 locators, err = s.wallet.BlockLocators(ctx, nil) 465 if err != nil { 466 return err 467 } 468 } 469 s.fetchHeadersFinished() 470 471 rescanPoint, err = s.wallet.RescanPoint(ctx) 472 if err != nil { 473 return err 474 } 475 if rescanPoint != nil { 476 s.mu.Lock() 477 discoverAccts := s.discoverAccts 478 s.mu.Unlock() 479 s.discoverAddressesStart() 480 err = s.wallet.DiscoverActiveAddresses(ctx, s.rpc, rescanPoint, discoverAccts, s.wallet.GapLimit()) 481 if err != nil { 482 return err 483 } 484 s.discoverAddressesFinished() 485 s.mu.Lock() 486 s.discoverAccts = false 487 s.mu.Unlock() 488 err = s.wallet.LoadActiveDataFilters(ctx, s.rpc, true) 489 if err != nil { 490 return err 491 } 492 493 s.rescanStart() 494 rescanBlock, err := s.wallet.BlockHeader(ctx, rescanPoint) 495 if err != nil { 496 return err 497 } 498 progress := make(chan wallet.RescanProgress, 1) 499 go s.wallet.RescanProgressFromHeight(ctx, s.rpc, int32(rescanBlock.Height), progress) 500 501 for p := range progress { 502 if p.Err != nil { 503 return p.Err 504 } 505 s.rescanProgress(p.ScannedThrough) 506 } 507 s.rescanFinished() 508 509 } else { 510 err = s.wallet.LoadActiveDataFilters(ctx, s.rpc, true) 511 if err != nil { 512 return err 513 } 514 } 515 s.synced() 516 517 // Rebroadcast unmined transactions 518 err = s.wallet.PublishUnminedTransactions(ctx, s.rpc) 519 if err != nil { 520 // Returning this error would end and (likely) restart sync in 521 // an endless loop. It's possible a transaction should be 522 // removed, but this is difficult to reliably detect over RPC. 523 log.Warnf("Could not publish one or more unmined transactions: %v", err) 524 } 525 526 err = s.rpc.Call(ctx, "rebroadcastwinners", nil) 527 if err != nil { 528 return err 529 } 530 531 log.Infof("Blockchain sync completed, wallet ready for general usage.") 532 533 // Wait for notifications to finish before returning 534 defer func() { 535 <-s.notifier.closed 536 }() 537 538 select { 539 case <-ctx.Done(): 540 client.Close() 541 return ctx.Err() 542 case <-client.Done(): 543 return client.Err() 544 } 545 } 546 547 type notifier struct { 548 atomicClosed uint32 549 syncer *Syncer 550 ctx context.Context 551 closed chan struct{} 552 connectingBlocks bool 553 } 554 555 func (n *notifier) Notify(method string, params json.RawMessage) error { 556 s := n.syncer 557 op := errors.Op(method) 558 ctx, task := trace.NewTask(n.ctx, method) 559 defer task.End() 560 switch method { 561 case "winningtickets": 562 err := s.winningTickets(ctx, params) 563 if err != nil { 564 log.Error(errors.E(op, err)) 565 } 566 case "blockconnected": 567 err := s.blockConnected(ctx, params) 568 if err == nil { 569 n.connectingBlocks = true 570 return nil 571 } 572 err = errors.E(op, err) 573 if !n.connectingBlocks { 574 log.Errorf("Failed to connect block: %v", err) 575 return nil 576 } 577 return err 578 case "relevanttxaccepted": 579 err := s.relevantTxAccepted(ctx, params) 580 if err != nil { 581 log.Error(errors.E(op, err)) 582 } 583 case "tspend": 584 err := s.storeTSpend(ctx, params) 585 if err != nil { 586 log.Error(errors.E(op, err)) 587 } 588 } 589 return nil 590 } 591 592 func (n *notifier) Close() error { 593 if atomic.CompareAndSwapUint32(&n.atomicClosed, 0, 1) { 594 close(n.closed) 595 } 596 return nil 597 } 598 599 func (s *Syncer) winningTickets(ctx context.Context, params json.RawMessage) error { 600 block, height, winners, err := dcrd.WinningTickets(params) 601 if err != nil { 602 return err 603 } 604 return s.wallet.VoteOnOwnedTickets(ctx, winners, block, height) 605 } 606 607 func (s *Syncer) blockConnected(ctx context.Context, params json.RawMessage) error { 608 header, relevant, err := dcrd.BlockConnected(params) 609 if err != nil { 610 return err 611 } 612 613 blockHash := header.BlockHash() 614 filter, proofIndex, proof, err := s.rpc.CFilterV2(ctx, &blockHash) 615 if err != nil { 616 return err 617 } 618 619 cnet := s.wallet.ChainParams().Net 620 err = validate.CFilterV2HeaderCommitment(cnet, header, filter, proofIndex, proof) 621 if err != nil { 622 return err 623 } 624 625 s.sidechainsMu.Lock() 626 defer s.sidechainsMu.Unlock() 627 628 blockNode := wallet.NewBlockNode(header, &blockHash, filter) 629 if wallet.BadCheckpoint(cnet, &blockHash, int32(header.Height)) { 630 blockNode.BadCheckpoint() 631 } 632 s.sidechains.AddBlockNode(blockNode) 633 s.relevantTxs[blockHash] = relevant 634 635 bestChain, err := s.wallet.EvaluateBestChain(ctx, &s.sidechains) 636 if err != nil { 637 return err 638 } 639 if len(bestChain) != 0 { 640 var prevChain []*wallet.BlockNode 641 prevChain, err = s.wallet.ChainSwitch(ctx, &s.sidechains, bestChain, s.relevantTxs) 642 if err != nil { 643 return err 644 } 645 646 if len(prevChain) != 0 { 647 log.Infof("Reorganize from %v to %v (total %d block(s) reorged)", 648 prevChain[len(prevChain)-1].Hash, bestChain[len(bestChain)-1].Hash, len(prevChain)) 649 for _, n := range prevChain { 650 s.sidechains.AddBlockNode(n) 651 652 // TODO: should add txs from the removed blocks 653 // to relevantTxs. Later block connected logs 654 // will be missing the transaction counts if a 655 // reorg switches back to this older chain. 656 } 657 } 658 for _, n := range bestChain { 659 log.Infof("Connected block %v, height %d, %d wallet transaction(s)", 660 n.Hash, n.Header.Height, len(s.relevantTxs[*n.Hash])) 661 delete(s.relevantTxs, *n.Hash) 662 } 663 } else { 664 log.Infof("Observed sidechain or orphan block %v (height %d)", &blockHash, header.Height) 665 } 666 667 return nil 668 } 669 670 func (s *Syncer) relevantTxAccepted(ctx context.Context, params json.RawMessage) error { 671 tx, err := dcrd.RelevantTxAccepted(params) 672 if err != nil { 673 return err 674 } 675 if s.wallet.ManualTickets() && stake.IsSStx(tx) { 676 return nil 677 } 678 return s.wallet.AddTransaction(ctx, tx, nil) 679 } 680 681 func (s *Syncer) storeTSpend(ctx context.Context, params json.RawMessage) error { 682 tx, err := dcrd.TSpend(params) 683 if err != nil { 684 return err 685 } 686 return s.wallet.AddTSpend(*tx) 687 }