github.com/decred/dcrlnd@v0.7.6/chainscan/csdrivers/dcrwdriver.go (about)

     1  package csdrivers
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	"decred.org/dcrwallet/v4/errors"
     9  	"decred.org/dcrwallet/v4/wallet"
    10  	"github.com/decred/dcrd/chaincfg/chainhash"
    11  	"github.com/decred/dcrd/gcs/v4"
    12  	"github.com/decred/dcrd/wire"
    13  	"github.com/decred/dcrlnd/blockcache"
    14  	"github.com/decred/dcrlnd/chainscan"
    15  )
    16  
    17  type cfilter struct {
    18  	height int32
    19  	hash   chainhash.Hash
    20  	key    [16]byte
    21  	filter *gcs.FilterV2
    22  }
    23  
    24  type eventReader struct {
    25  	ctx context.Context
    26  	c   chan chainscan.ChainEvent
    27  }
    28  
    29  // DcrwalletCSDriver...
    30  //
    31  // NOTE: Using an individual driver instance for more than either a single
    32  // historical or tip watcher scanner leads to undefined behavior.
    33  type DcrwalletCSDriver struct {
    34  	w *wallet.Wallet
    35  
    36  	blockCache *blockcache.BlockCache
    37  
    38  	mtx      sync.Mutex
    39  	ereaders []*eventReader
    40  
    41  	// The following are members that define a cfilter cache which is
    42  	// useful to reduce db contention by reading cfilters in batches.
    43  	cache            []cfilter
    44  	cacheStartHeight int32
    45  }
    46  
    47  // Type assertions to ensure the driver fulfills the correct interfaces.
    48  var _ chainscan.HistoricalChainSource = (*DcrwalletCSDriver)(nil)
    49  var _ chainscan.TipChainSource = (*DcrwalletCSDriver)(nil)
    50  
    51  // Hint for the capacity of the cache size. Value is arbitrary at this point.
    52  var cacheCapHint = 200
    53  
    54  func NewDcrwalletCSDriver(w *wallet.Wallet, bCache *blockcache.BlockCache) *DcrwalletCSDriver {
    55  	return &DcrwalletCSDriver{
    56  		w:          w,
    57  		blockCache: bCache,
    58  		cache:      make([]cfilter, 0, cacheCapHint),
    59  	}
    60  }
    61  
    62  func (d *DcrwalletCSDriver) signalEventReaders(e chainscan.ChainEvent) {
    63  	d.mtx.Lock()
    64  	readers := d.ereaders
    65  	d.mtx.Unlock()
    66  
    67  	for _, er := range readers {
    68  		select {
    69  		case <-er.ctx.Done():
    70  		case er.c <- e:
    71  		}
    72  	}
    73  }
    74  
    75  // Run runs the driver. It should be executed on a separate goroutine. This is
    76  // only needed if the NextTip() function is going to be used such as when using
    77  // this driver as a source for a TipWatcher scanner.
    78  func (d *DcrwalletCSDriver) Run(ctx context.Context) error {
    79  	log.Debugf("Running chainscan driver with embedded dcrwallet")
    80  	client := d.w.NtfnServer.TransactionNotifications()
    81  	defer client.Done()
    82  
    83  	for {
    84  		select {
    85  		case <-ctx.Done():
    86  			return ctx.Err()
    87  		case ntfn := <-client.C:
    88  			for _, b := range ntfn.DetachedBlocks {
    89  				e := chainscan.BlockDisconnectedEvent{
    90  					Hash:     b.BlockHash(),
    91  					Height:   int32(b.Height),
    92  					PrevHash: b.PrevBlock,
    93  					Header:   b,
    94  				}
    95  
    96  				d.signalEventReaders(e)
    97  			}
    98  
    99  			for _, b := range ntfn.AttachedBlocks {
   100  				if b.Header == nil {
   101  					// Shouldn't happen, but play it safe.
   102  					continue
   103  				}
   104  
   105  				hash := b.Header.BlockHash()
   106  				key, filter, err := d.w.CFilterV2(ctx, &hash)
   107  				if err != nil {
   108  					log.Errorf("Error obtaining cfilter from wallet: %v", err)
   109  					return err
   110  				}
   111  
   112  				e := chainscan.BlockConnectedEvent{
   113  					PrevHash: b.Header.PrevBlock,
   114  					Hash:     hash,
   115  					Height:   int32(b.Header.Height),
   116  					CFKey:    key,
   117  					Filter:   filter,
   118  					Header:   b.Header,
   119  				}
   120  
   121  				d.signalEventReaders(e)
   122  			}
   123  		}
   124  	}
   125  
   126  }
   127  
   128  func (d *DcrwalletCSDriver) ChainEvents(ctx context.Context) <-chan chainscan.ChainEvent {
   129  	er := &eventReader{
   130  		ctx: ctx,
   131  		c:   make(chan chainscan.ChainEvent),
   132  	}
   133  	d.mtx.Lock()
   134  	d.ereaders = append(d.ereaders, er)
   135  	d.mtx.Unlock()
   136  
   137  	return er.c
   138  }
   139  
   140  // GetBlock returns the given block for the given blockhash. Note that for
   141  // wallets running in SPV mode this blocks until the wallet is connected to a
   142  // peer and it correctly returns a full block.
   143  func (d *DcrwalletCSDriver) GetBlock(ctx context.Context, bh *chainhash.Hash) (*wire.MsgBlock, error) {
   144  	return d.blockCache.GetBlock(ctx, bh, d.getBlock)
   145  }
   146  
   147  // getBlock returns the given block for the given blockhash. Note that for
   148  // wallets running in SPV mode this blocks until the wallet is connected to a
   149  // peer and it correctly returns a full block.
   150  func (d *DcrwalletCSDriver) getBlock(ctx context.Context, bh *chainhash.Hash) (*wire.MsgBlock, error) {
   151  getblock:
   152  	for {
   153  		// Keep trying to get the network backend until the context is
   154  		// canceled.
   155  		n, err := d.w.NetworkBackend()
   156  		if errors.Is(err, errors.NoPeers) {
   157  			select {
   158  			case <-ctx.Done():
   159  				return nil, ctx.Err()
   160  			case <-time.After(time.Second):
   161  				continue getblock
   162  			}
   163  		}
   164  
   165  		blocks, err := n.Blocks(ctx, []*chainhash.Hash{bh})
   166  		if len(blocks) > 0 && err == nil {
   167  			return blocks[0], nil
   168  		}
   169  
   170  		// The syncer might have failed due to any number of reasons,
   171  		// but it's likely it will come back online shortly. So wait
   172  		// until we can try again.
   173  		select {
   174  		case <-ctx.Done():
   175  			return nil, ctx.Err()
   176  		case <-time.After(time.Second):
   177  		}
   178  	}
   179  }
   180  
   181  func (d *DcrwalletCSDriver) CurrentTip(ctx context.Context) (*chainhash.Hash, int32, error) {
   182  	bh, h := d.w.MainChainTip(ctx)
   183  	return &bh, h, nil
   184  }
   185  
   186  // GetCFilter is part of the chainscan.HistoricalChainSource interface.
   187  //
   188  // NOTE: The returned chainhash pointer is not safe for storage as it belongs
   189  // to a cache entry. This is fine for use on a chainscan.Historical scanner
   190  // since it never stores or leaks the pointer itself.
   191  func (d *DcrwalletCSDriver) GetCFilter(ctx context.Context, height int32) (*chainhash.Hash, [16]byte, *gcs.FilterV2, error) {
   192  	// Fast track when data is in memory.
   193  	if height >= d.cacheStartHeight && height < d.cacheStartHeight+int32(len(d.cache)) {
   194  		i := int(height - d.cacheStartHeight)
   195  		c := &d.cache[i]
   196  		return &c.hash, c.key, c.filter, nil
   197  	}
   198  
   199  	// Read a bunch of cfilters in one go since it's likely we'll be
   200  	// queried for the next few.
   201  	start := wallet.NewBlockIdentifierFromHeight(height)
   202  	i := 0
   203  	d.cache = d.cache[:cap(d.cache)]
   204  	rangeFn := func(bh chainhash.Hash, key [16]byte, filter *gcs.FilterV2) (bool, error) {
   205  		d.cache[i] = cfilter{
   206  			hash:   bh,
   207  			height: height + int32(i),
   208  			key:    key,
   209  			filter: filter,
   210  		}
   211  
   212  		// Stop if the cache has been filled.
   213  		i++
   214  		return i >= cap(d.cache), nil
   215  	}
   216  	err := d.w.RangeCFiltersV2(ctx, start, nil, rangeFn)
   217  	if err != nil {
   218  		return nil, [16]byte{}, nil, err
   219  	}
   220  
   221  	// If we didn't read any filters from the db, it means we were
   222  	// requested a filter past the current mainchain tip. Inform the
   223  	// appropriate error in this case.
   224  	if i == 0 {
   225  		return nil, [16]byte{}, nil, chainscan.ErrBlockAfterTip{Height: height}
   226  	}
   227  
   228  	// Clear out unused entries so we don't keep a reference to the filters
   229  	// forever.
   230  	for j := i; j < cap(d.cache); j++ {
   231  		d.cache[j].filter = nil
   232  	}
   233  
   234  	// Keep track of correct cache start and size.
   235  	d.cache = d.cache[:i]
   236  	d.cacheStartHeight = height
   237  
   238  	// The desired filter is the first one.
   239  	c := &d.cache[0]
   240  	return &c.hash, c.key, c.filter, nil
   241  }