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 = &notifier{
   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  }