decred.org/dcrwallet/v3@v3.1.0/spv/backend.go (about)

     1  // Copyright (c) 2018-2019 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 spv
     6  
     7  import (
     8  	"context"
     9  	"runtime"
    10  	"sync"
    11  
    12  	"decred.org/dcrwallet/v3/errors"
    13  	"decred.org/dcrwallet/v3/p2p"
    14  	"decred.org/dcrwallet/v3/validate"
    15  	"decred.org/dcrwallet/v3/wallet"
    16  	"github.com/decred/dcrd/chaincfg/chainhash"
    17  	"github.com/decred/dcrd/dcrutil/v4"
    18  	"github.com/decred/dcrd/gcs/v4"
    19  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    20  	"github.com/decred/dcrd/wire"
    21  )
    22  
    23  var _ wallet.NetworkBackend = (*Syncer)(nil)
    24  
    25  // TODO: When using the Syncer as a NetworkBackend, keep track of in-flight
    26  // blocks and cfilters.  If one is already incoming, wait on that response.  If
    27  // that peer is lost, try a different peer.  Optionally keep a cache of fetched
    28  // data so it can be immediately returned without another call.
    29  
    30  func pickAny(*p2p.RemotePeer) bool { return true }
    31  
    32  // Blocks implements the Blocks method of the wallet.Peer interface.
    33  func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) {
    34  	for {
    35  		if err := ctx.Err(); err != nil {
    36  			return nil, err
    37  		}
    38  		rp, err := s.pickRemote(pickAny)
    39  		if err != nil {
    40  			return nil, err
    41  		}
    42  		blocks, err := rp.Blocks(ctx, blockHashes)
    43  		if err != nil {
    44  			continue
    45  		}
    46  		return blocks, nil
    47  	}
    48  }
    49  
    50  // filterProof is an alias to the same anonymous struct as wallet package's
    51  // FilterProof struct.
    52  type filterProof = struct {
    53  	Filter     *gcs.FilterV2
    54  	ProofIndex uint32
    55  	Proof      []chainhash.Hash
    56  }
    57  
    58  // CFiltersV2 implements the CFiltersV2 method of the wallet.Peer interface.
    59  func (s *Syncer) CFiltersV2(ctx context.Context, blockHashes []*chainhash.Hash) ([]filterProof, error) {
    60  	for {
    61  		if err := ctx.Err(); err != nil {
    62  			return nil, err
    63  		}
    64  		rp, err := s.pickRemote(pickAny)
    65  		if err != nil {
    66  			return nil, err
    67  		}
    68  		fs, err := rp.CFiltersV2(ctx, blockHashes)
    69  		if err != nil {
    70  			continue
    71  		}
    72  		return fs, nil
    73  	}
    74  }
    75  
    76  // Headers implements the Headers method of the wallet.Peer interface.
    77  func (s *Syncer) Headers(ctx context.Context, blockLocators []*chainhash.Hash, hashStop *chainhash.Hash) ([]*wire.BlockHeader, error) {
    78  	for {
    79  		if err := ctx.Err(); err != nil {
    80  			return nil, err
    81  		}
    82  		rp, err := s.pickRemote(pickAny)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		hs, err := rp.Headers(ctx, blockLocators, hashStop)
    87  		if err != nil {
    88  			continue
    89  		}
    90  		return hs, nil
    91  	}
    92  }
    93  
    94  func (s *Syncer) String() string {
    95  	// This method is part of the wallet.Peer interface and will typically
    96  	// specify the remote address of the peer.  Since the syncer can encompass
    97  	// multiple peers, just use the qualified type as the string.
    98  	return "spv.Syncer"
    99  }
   100  
   101  // LoadTxFilter implements the LoadTxFilter method of the wallet.NetworkBackend
   102  // interface.
   103  //
   104  // NOTE: due to blockcf2 *not* including the spent outpoints in the block, the
   105  // addrs[] slice MUST include the addresses corresponding to the respective
   106  // outpoints, otherwise they will not be returned during the rescan.
   107  func (s *Syncer) LoadTxFilter(ctx context.Context, reload bool, addrs []stdaddr.Address, outpoints []wire.OutPoint) error {
   108  	s.filterMu.Lock()
   109  	if reload || s.rescanFilter == nil {
   110  		s.rescanFilter = wallet.NewRescanFilter(nil, nil)
   111  		s.filterData = nil
   112  	}
   113  	for _, addr := range addrs {
   114  		_, pkScript := addr.PaymentScript()
   115  		s.rescanFilter.AddAddress(addr)
   116  		s.filterData.AddRegularPkScript(pkScript)
   117  	}
   118  	for i := range outpoints {
   119  		s.rescanFilter.AddUnspentOutPoint(&outpoints[i])
   120  	}
   121  	s.filterMu.Unlock()
   122  	return nil
   123  }
   124  
   125  // PublishTransactions implements the PublishTransaction method of the
   126  // wallet.Peer interface.
   127  func (s *Syncer) PublishTransactions(ctx context.Context, txs ...*wire.MsgTx) error {
   128  	// Figure out transactions that are not stored by the wallet and create
   129  	// an aux map so we can choose which need to be stored in the syncer's
   130  	// mempool.
   131  	walletBacked := make(map[chainhash.Hash]bool, len(txs))
   132  	relevant, _, err := s.wallet.DetermineRelevantTxs(ctx, txs...)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	for _, tx := range relevant {
   137  		walletBacked[tx.TxHash()] = true
   138  	}
   139  
   140  	msg := wire.NewMsgInvSizeHint(uint(len(txs)))
   141  	for _, tx := range txs {
   142  		txHash := tx.TxHash()
   143  		if !walletBacked[txHash] {
   144  			// Load into the mempool and let the mempool handler
   145  			// know of it.
   146  			if _, loaded := s.mempool.LoadOrStore(txHash, tx); !loaded {
   147  				select {
   148  				case s.mempoolAdds <- &txHash:
   149  				case <-ctx.Done():
   150  					return ctx.Err()
   151  				}
   152  			}
   153  		}
   154  		err := msg.AddInvVect(wire.NewInvVect(wire.InvTypeTx, &txHash))
   155  		if err != nil {
   156  			return errors.E(errors.Protocol, err)
   157  		}
   158  	}
   159  	return s.forRemotes(func(rp *p2p.RemotePeer) error {
   160  		for _, inv := range msg.InvList {
   161  			rp.InvsSent().Add(inv.Hash)
   162  		}
   163  		return rp.SendMessage(ctx, msg)
   164  	})
   165  }
   166  
   167  // Rescan implements the Rescan method of the wallet.NetworkBackend interface.
   168  func (s *Syncer) Rescan(ctx context.Context, blockHashes []chainhash.Hash, save func(*chainhash.Hash, []*wire.MsgTx) error) error {
   169  	const op errors.Op = "spv.Rescan"
   170  
   171  	cfilters := make([]*gcs.FilterV2, 0, len(blockHashes))
   172  	cfilterKeys := make([][gcs.KeySize]byte, 0, len(blockHashes))
   173  	for i := 0; i < len(blockHashes); i++ {
   174  		k, f, err := s.wallet.CFilterV2(ctx, &blockHashes[i])
   175  		if err != nil {
   176  			return err
   177  		}
   178  		cfilters = append(cfilters, f)
   179  		cfilterKeys = append(cfilterKeys, k)
   180  	}
   181  
   182  	blockMatches := make([]*wire.MsgBlock, len(blockHashes)) // Block assigned to slice once fetched
   183  
   184  	// Read current filter data.  filterData is reassinged to new data matches
   185  	// for subsequent filter checks, which improves filter matching performance
   186  	// by checking for less data.
   187  	s.filterMu.Lock()
   188  	filterData := s.filterData
   189  	s.filterMu.Unlock()
   190  
   191  	idx := 0
   192  FilterLoop:
   193  	for idx < len(blockHashes) {
   194  		var fmatches []*chainhash.Hash
   195  		var fmatchidx []int
   196  		var fmatchMu sync.Mutex
   197  
   198  		// Spawn ncpu workers to check filter matches
   199  		ncpu := runtime.NumCPU()
   200  		c := make(chan int, ncpu)
   201  		var wg sync.WaitGroup
   202  		wg.Add(ncpu)
   203  		for i := 0; i < ncpu; i++ {
   204  			go func() {
   205  				for i := range c {
   206  					blockHash := &blockHashes[i]
   207  					key := cfilterKeys[i]
   208  					f := cfilters[i]
   209  					if f.MatchAny(key, filterData) {
   210  						fmatchMu.Lock()
   211  						fmatches = append(fmatches, blockHash)
   212  						fmatchidx = append(fmatchidx, i)
   213  						fmatchMu.Unlock()
   214  					}
   215  				}
   216  				wg.Done()
   217  			}()
   218  		}
   219  		for i := idx; i < len(blockHashes); i++ {
   220  			if blockMatches[i] != nil {
   221  				// Already fetched this block
   222  				continue
   223  			}
   224  			c <- i
   225  		}
   226  		close(c)
   227  		wg.Wait()
   228  
   229  		if len(fmatches) != 0 {
   230  			var rp *p2p.RemotePeer
   231  		PickPeer:
   232  			for {
   233  				if err := ctx.Err(); err != nil {
   234  					return err
   235  				}
   236  				if rp == nil {
   237  					var err error
   238  					rp, err = s.pickRemote(pickAny)
   239  					if err != nil {
   240  						return err
   241  					}
   242  				}
   243  
   244  				blocks, err := rp.Blocks(ctx, fmatches)
   245  				if err != nil {
   246  					rp = nil
   247  					continue PickPeer
   248  				}
   249  
   250  				for j, b := range blocks {
   251  					// Validate fetched blocks before rescanning transactions.  PoW
   252  					// and PoS difficulties have already been validated since the
   253  					// header is saved by the wallet, and modifications to these in
   254  					// the downloaded block would result in a different block hash
   255  					// and failure to fetch the block.
   256  					//
   257  					// Block filters were also validated
   258  					// against the header (assuming dcp0005
   259  					// was activated).
   260  					err = validate.MerkleRoots(b)
   261  					if err != nil {
   262  						err = validate.DCP0005MerkleRoot(b)
   263  					}
   264  					if err != nil {
   265  						err := errors.E(op, err)
   266  						rp.Disconnect(err)
   267  						rp = nil
   268  						continue PickPeer
   269  					}
   270  
   271  					i := fmatchidx[j]
   272  					blockMatches[i] = b
   273  				}
   274  				break
   275  			}
   276  		}
   277  
   278  		for i := idx; i < len(blockMatches); i++ {
   279  			b := blockMatches[i]
   280  			if b == nil {
   281  				// No filter match, skip block
   282  				continue
   283  			}
   284  
   285  			if err := ctx.Err(); err != nil {
   286  				return err
   287  			}
   288  
   289  			matchedTxs, fadded := s.rescanBlock(b)
   290  			if len(matchedTxs) != 0 {
   291  				err := save(&blockHashes[i], matchedTxs)
   292  				if err != nil {
   293  					return err
   294  				}
   295  
   296  				// Check for more matched blocks using updated filters,
   297  				// starting at the next block.
   298  				if len(fadded) != 0 {
   299  					idx = i + 1
   300  					filterData = fadded
   301  					continue FilterLoop
   302  				}
   303  			}
   304  		}
   305  		return nil
   306  	}
   307  
   308  	return nil
   309  }
   310  
   311  // StakeDifficulty implements the StakeDifficulty method of the
   312  // wallet.NetworkBackend interface.
   313  //
   314  // This implementation of the method will always error as the stake difficulty
   315  // is not queryable over wire protocol, and when the next stake difficulty is
   316  // available in a header commitment, the wallet will be able to determine this
   317  // itself without requiring the NetworkBackend.
   318  func (s *Syncer) StakeDifficulty(ctx context.Context) (dcrutil.Amount, error) {
   319  	return 0, errors.E(errors.Invalid, "stake difficulty is not queryable over wire protocol")
   320  }