github.com/decred/dcrlnd@v0.7.6/routing/chainview/dcrd.go (about)

     1  package chainview
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"sync"
     9  	"sync/atomic"
    10  
    11  	"github.com/decred/dcrd/chaincfg/chainhash"
    12  	jsontypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    13  	"github.com/decred/dcrd/rpcclient/v8"
    14  	"github.com/decred/dcrd/wire"
    15  	"github.com/decred/dcrlnd/channeldb"
    16  )
    17  
    18  // DcrdFilteredChainView is an implementation of the FilteredChainView
    19  // interface which is backed by an active websockets connection to dcrd.
    20  type DcrdFilteredChainView struct {
    21  	started int32 // To be used atomically.
    22  	stopped int32 // To be used atomically.
    23  
    24  	// bestHeight is the height of the latest block added to the
    25  	// blockQueue from the onFilteredConnectedMethod. It is used to
    26  	// determine up to what height we would need to rescan in case
    27  	// of a filter update.
    28  	bestHeightMtx sync.Mutex
    29  	bestHeight    int64
    30  
    31  	dcrdConn *rpcclient.Client
    32  
    33  	// blockEventQueue is the ordered queue used to keep the order
    34  	// of connected and disconnected blocks sent to the reader of the
    35  	// chainView.
    36  	blockQueue *blockEventQueue
    37  
    38  	// filterUpdates is a channel in which updates to the utxo filter
    39  	// attached to this instance are sent over.
    40  	filterUpdates chan filterUpdate
    41  
    42  	// chainFilter is the set of utox's that we're currently watching
    43  	// spends for within the chain.
    44  	filterMtx   sync.RWMutex
    45  	chainFilter map[wire.OutPoint]struct{}
    46  
    47  	// filterBlockReqs is a channel in which requests to filter select
    48  	// blocks will be sent over.
    49  	filterBlockReqs chan *filterBlockReq
    50  
    51  	quit chan struct{}
    52  	wg   sync.WaitGroup
    53  }
    54  
    55  // A compile time check to ensure DcrdFilteredChainView implements the
    56  // chainview.FilteredChainView.
    57  var _ FilteredChainView = (*DcrdFilteredChainView)(nil)
    58  
    59  // NewDcrdFilteredChainView creates a new instance of a FilteredChainView from
    60  // RPC credentials for an active dcrd instance.
    61  func NewDcrdFilteredChainView(config rpcclient.ConnConfig) (*DcrdFilteredChainView, error) {
    62  	chainView := &DcrdFilteredChainView{
    63  		chainFilter:     make(map[wire.OutPoint]struct{}),
    64  		filterUpdates:   make(chan filterUpdate),
    65  		filterBlockReqs: make(chan *filterBlockReq),
    66  		quit:            make(chan struct{}),
    67  	}
    68  
    69  	ntfnCallbacks := &rpcclient.NotificationHandlers{
    70  		OnBlockConnected:    chainView.onBlockConnected,
    71  		OnBlockDisconnected: chainView.onBlockDisconnected,
    72  	}
    73  
    74  	// Disable connecting to dcrd within the rpcclient.New method. We
    75  	// defer establishing the connection to our .Start() method.
    76  	config.DisableConnectOnNew = true
    77  	config.DisableAutoReconnect = false
    78  	chainConn, err := rpcclient.New(&config, ntfnCallbacks)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	chainView.dcrdConn = chainConn
    83  
    84  	chainView.blockQueue = newBlockEventQueue()
    85  
    86  	return chainView, nil
    87  }
    88  
    89  // Start starts all goroutines necessary for normal operation.
    90  //
    91  // NOTE: This is part of the FilteredChainView interface.
    92  func (b *DcrdFilteredChainView) Start() error {
    93  	// Already started?
    94  	if atomic.AddInt32(&b.started, 1) != 1 {
    95  		return nil
    96  	}
    97  
    98  	log.Infof("FilteredChainView starting")
    99  
   100  	// Connect to dcrd, and register for notifications on connected, and
   101  	// disconnected blocks.
   102  	if err := b.dcrdConn.Connect(context.Background(), true); err != nil {
   103  		return err
   104  	}
   105  	if err := b.dcrdConn.NotifyBlocks(context.TODO()); err != nil {
   106  		return err
   107  	}
   108  
   109  	_, bestHeight, err := b.dcrdConn.GetBestBlock(context.TODO())
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	b.bestHeightMtx.Lock()
   115  	b.bestHeight = bestHeight
   116  	b.bestHeightMtx.Unlock()
   117  
   118  	b.blockQueue.Start()
   119  
   120  	b.wg.Add(1)
   121  	go b.chainFilterer()
   122  
   123  	return nil
   124  }
   125  
   126  // Stop stops all goroutines which we launched by the prior call to the Start
   127  // method.
   128  //
   129  // NOTE: This is part of the FilteredChainView interface.
   130  func (b *DcrdFilteredChainView) Stop() error {
   131  	// Already shutting down?
   132  	if atomic.AddInt32(&b.stopped, 1) != 1 {
   133  		return nil
   134  	}
   135  
   136  	// Shutdown the rpc client, this gracefully disconnects from dcrd, and
   137  	// cleans up all related resources.
   138  	b.dcrdConn.Shutdown()
   139  
   140  	b.blockQueue.Stop()
   141  
   142  	log.Infof("FilteredChainView stopping")
   143  
   144  	close(b.quit)
   145  	b.wg.Wait()
   146  
   147  	return nil
   148  }
   149  
   150  // onBlockConnected is called for each block that's connected to the end of the
   151  // main chain. Based on our current chain filter, the block may or may not
   152  // include any relevant transactions.
   153  func (b *DcrdFilteredChainView) onBlockConnected(blockHeader []byte, txns [][]byte) {
   154  	var header wire.BlockHeader
   155  	if err := header.FromBytes(blockHeader); err != nil {
   156  		log.Warnf("Received block connected with malformed header: %v", err)
   157  		return
   158  	}
   159  
   160  	mtxs := make([]*wire.MsgTx, len(txns))
   161  	b.filterMtx.Lock()
   162  	for i, txBytes := range txns {
   163  		var mtx wire.MsgTx
   164  		if err := mtx.FromBytes(txBytes); err != nil {
   165  			log.Warnf("Received block connected with malformed tx: %v", err)
   166  			return
   167  		}
   168  		mtxs[i] = &mtx
   169  	}
   170  
   171  	for _, mtx := range mtxs {
   172  		for _, txIn := range mtx.TxIn {
   173  			// We can delete this outpoint from the chainFilter, as
   174  			// we just received a block where it was spent. In case
   175  			// of a reorg, this outpoint might get "un-spent", but
   176  			// that's okay since it would never be wise to consider
   177  			// the channel open again (since a spending transaction
   178  			// exists on the network).
   179  			delete(b.chainFilter, txIn.PreviousOutPoint)
   180  		}
   181  	}
   182  	b.filterMtx.Unlock()
   183  
   184  	// We record the height of the last connected block added to the
   185  	// blockQueue such that we can scan up to this height in case of
   186  	// a rescan. It must be protected by a mutex since a filter update
   187  	// might be trying to read it concurrently.
   188  	b.bestHeightMtx.Lock()
   189  	b.bestHeight = int64(header.Height)
   190  	b.bestHeightMtx.Unlock()
   191  
   192  	block := &FilteredBlock{
   193  		Hash:         header.BlockHash(),
   194  		Height:       int64(header.Height),
   195  		Transactions: mtxs,
   196  	}
   197  
   198  	b.blockQueue.Add(&blockEvent{
   199  		eventType: connected,
   200  		block:     block,
   201  	})
   202  
   203  }
   204  
   205  // onBlockDisconnected is a callback which is executed once a block is
   206  // disconnected from the end of the main chain.
   207  func (b *DcrdFilteredChainView) onBlockDisconnected(blockHeader []byte) {
   208  	var header wire.BlockHeader
   209  	if err := header.FromBytes(blockHeader); err != nil {
   210  		log.Warnf("Received block disconnected with malformed header: %v", err)
   211  		return
   212  	}
   213  
   214  	log.Debugf("got disconnected block at height %d: %v", header.Height,
   215  		header.BlockHash())
   216  
   217  	filteredBlock := &FilteredBlock{
   218  		Hash:   header.BlockHash(),
   219  		Height: int64(header.Height),
   220  	}
   221  
   222  	b.blockQueue.Add(&blockEvent{
   223  		eventType: disconnected,
   224  		block:     filteredBlock,
   225  	})
   226  }
   227  
   228  // filterBlockReq houses a request to manually filter a block specified by
   229  // block hash.
   230  type filterBlockReq struct {
   231  	blockHash *chainhash.Hash
   232  	resp      chan *FilteredBlock
   233  	err       chan error
   234  }
   235  
   236  // FilterBlock takes a block hash, and returns a FilteredBlocks which is the
   237  // result of applying the current registered UTXO sub-set on the block
   238  // corresponding to that block hash. If any watched UTOX's are spent by the
   239  // selected lock, then the internal chainFilter will also be updated.
   240  //
   241  // NOTE: This is part of the FilteredChainView interface.
   242  func (b *DcrdFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) {
   243  	req := &filterBlockReq{
   244  		blockHash: blockHash,
   245  		resp:      make(chan *FilteredBlock, 1),
   246  		err:       make(chan error, 1),
   247  	}
   248  
   249  	select {
   250  	case b.filterBlockReqs <- req:
   251  	case <-b.quit:
   252  		return nil, fmt.Errorf("FilteredChainView shutting down")
   253  	}
   254  
   255  	return <-req.resp, <-req.err
   256  }
   257  
   258  // chainFilterer is the primary goroutine which: listens for new blocks coming
   259  // and dispatches the relevant FilteredBlock notifications, updates the filter
   260  // due to requests by callers, and finally is able to preform targeted block
   261  // filtration.
   262  //
   263  // TODO(roasbeef): change to use loadfilter RPC's
   264  func (b *DcrdFilteredChainView) chainFilterer() {
   265  	defer b.wg.Done()
   266  
   267  	// filterBlock is a helper function that scans the given block, and
   268  	// notes which transactions spend outputs which are currently being
   269  	// watched. Additionally, the chain filter will also be updated by
   270  	// removing any spent outputs.
   271  	filterBlock := func(blk *wire.MsgBlock) []*wire.MsgTx {
   272  		b.filterMtx.Lock()
   273  		defer b.filterMtx.Unlock()
   274  
   275  		var filteredTxns []*wire.MsgTx
   276  		for _, tx := range blk.Transactions {
   277  			var txAlreadyFiltered bool
   278  			for _, txIn := range tx.TxIn {
   279  				prevOp := txIn.PreviousOutPoint
   280  				if _, ok := b.chainFilter[prevOp]; !ok {
   281  					continue
   282  				}
   283  
   284  				delete(b.chainFilter, prevOp)
   285  
   286  				// Only add this txn to our list of filtered
   287  				// txns if it is the first previous outpoint to
   288  				// cause a match.
   289  				if txAlreadyFiltered {
   290  					continue
   291  				}
   292  
   293  				filteredTxns = append(filteredTxns, tx)
   294  				txAlreadyFiltered = true
   295  
   296  			}
   297  		}
   298  
   299  		return filteredTxns
   300  	}
   301  
   302  	decodeJSONBlock := func(block *jsontypes.RescannedBlock,
   303  		height int64) (*FilteredBlock, error) {
   304  		hash, err := chainhash.NewHashFromStr(block.Hash)
   305  		if err != nil {
   306  			return nil, err
   307  		}
   308  		txs := make([]*wire.MsgTx, 0, len(block.Transactions))
   309  		for _, str := range block.Transactions {
   310  			b, err := hex.DecodeString(str)
   311  			if err != nil {
   312  				return nil, err
   313  			}
   314  			tx := &wire.MsgTx{}
   315  			err = tx.Deserialize(bytes.NewReader(b))
   316  			if err != nil {
   317  				return nil, err
   318  			}
   319  			txs = append(txs, tx)
   320  		}
   321  		return &FilteredBlock{
   322  			Hash:         *hash,
   323  			Height:       height,
   324  			Transactions: txs,
   325  		}, nil
   326  	}
   327  
   328  	for {
   329  		select {
   330  		// The caller has just sent an update to the current chain
   331  		// filter, so we'll apply the update, possibly rewinding our
   332  		// state partially.
   333  		case update := <-b.filterUpdates:
   334  
   335  			// First, we'll add all the new UTXO's to the set of
   336  			// watched UTXO's, eliminating any duplicates in the
   337  			// process.
   338  			log.Tracef("Updating chain filter with new UTXO's: %v",
   339  				update.newUtxos)
   340  
   341  			b.filterMtx.Lock()
   342  			for _, newOp := range update.newUtxos {
   343  				b.chainFilter[newOp] = struct{}{}
   344  			}
   345  			b.filterMtx.Unlock()
   346  
   347  			// Apply the new TX filter to dcrd, which will cause
   348  			// all following notifications from and calls to it
   349  			// return blocks filtered with the new filter.
   350  			b.dcrdConn.LoadTxFilter(context.TODO(), false, nil, update.newUtxos)
   351  
   352  			// All blocks gotten after we loaded the filter will
   353  			// have the filter applied, but we will need to rescan
   354  			// the blocks up to the height of the block we last
   355  			// added to the blockQueue.
   356  			b.bestHeightMtx.Lock()
   357  			bestHeight := b.bestHeight
   358  			b.bestHeightMtx.Unlock()
   359  
   360  			// If the update height matches our best known height,
   361  			// then we don't need to do any rewinding.
   362  			if update.updateHeight == bestHeight {
   363  				continue
   364  			}
   365  
   366  			// Otherwise, we'll rewind the state to ensure the
   367  			// caller doesn't miss any relevant notifications.
   368  			// Starting from the height _after_ the update height,
   369  			// we'll walk forwards, rescanning one block at a time
   370  			// with dcrd applying the newly loaded filter to each
   371  			// block.
   372  			for i := update.updateHeight + 1; i < bestHeight+1; i++ {
   373  				blockHash, err := b.dcrdConn.GetBlockHash(context.TODO(), i)
   374  				if err != nil {
   375  					log.Warnf("Unable to get block hash "+
   376  						"for block at height %d: %v",
   377  						i, err)
   378  					continue
   379  				}
   380  
   381  				// To avoid dealing with the case where a reorg
   382  				// is happening while we rescan, we scan one
   383  				// block at a time, skipping blocks that might
   384  				// have gone missing.
   385  				rescanned, err := b.dcrdConn.Rescan(
   386  					context.TODO(), []chainhash.Hash{*blockHash})
   387  				if err != nil {
   388  					log.Warnf("Unable to rescan block "+
   389  						"with hash %v at height %d: %v",
   390  						blockHash, i, err)
   391  					continue
   392  				}
   393  
   394  				// If no block was returned from the rescan, it
   395  				// means no matching transactions were found.
   396  				if len(rescanned.DiscoveredData) != 1 {
   397  					log.Tracef("rescan of block %v at "+
   398  						"height=%d yielded no "+
   399  						"transactions", blockHash, i)
   400  					continue
   401  				}
   402  				decoded, err := decodeJSONBlock(
   403  					&rescanned.DiscoveredData[0], i)
   404  				if err != nil {
   405  					log.Errorf("Unable to decode block: %v",
   406  						err)
   407  					continue
   408  				}
   409  				b.blockQueue.Add(&blockEvent{
   410  					eventType: connected,
   411  					block:     decoded,
   412  				})
   413  			}
   414  
   415  		// We've received a new request to manually filter a block.
   416  		case req := <-b.filterBlockReqs:
   417  			// First we'll fetch the block itself as well as some
   418  			// additional information including its height.
   419  			block, err := b.dcrdConn.GetBlock(context.TODO(), req.blockHash)
   420  			if err != nil {
   421  				req.err <- err
   422  				req.resp <- nil
   423  				continue
   424  			}
   425  			header, err := b.dcrdConn.GetBlockHeaderVerbose(context.TODO(), req.blockHash)
   426  			if err != nil {
   427  				req.err <- err
   428  				req.resp <- nil
   429  				continue
   430  			}
   431  
   432  			// Once we have this info, we can directly filter the
   433  			// block and dispatch the proper notification.
   434  			req.resp <- &FilteredBlock{
   435  				Hash:         *req.blockHash,
   436  				Height:       int64(header.Height),
   437  				Transactions: filterBlock(block),
   438  			}
   439  			req.err <- err
   440  
   441  		case <-b.quit:
   442  			return
   443  		}
   444  	}
   445  }
   446  
   447  // filterUpdate is a message sent to the chainFilterer to update the current
   448  // chainFilter state.
   449  type filterUpdate struct {
   450  	newUtxos     []wire.OutPoint
   451  	updateHeight int64
   452  }
   453  
   454  // UpdateFilter updates the UTXO filter which is to be consulted when creating
   455  // FilteredBlocks to be sent to subscribed clients. This method is cumulative
   456  // meaning repeated calls to this method should _expand_ the size of the UTXO
   457  // sub-set currently being watched.  If the set updateHeight is _lower_ than
   458  // the best known height of the implementation, then the state should be
   459  // rewound to ensure all relevant notifications are dispatched.
   460  //
   461  // NOTE: This is part of the FilteredChainView interface.
   462  func (b *DcrdFilteredChainView) UpdateFilter(ops []channeldb.EdgePoint,
   463  	updateHeight int64) error {
   464  
   465  	newUtxos := make([]wire.OutPoint, len(ops))
   466  	for i, op := range ops {
   467  		newUtxos[i] = op.OutPoint
   468  	}
   469  
   470  	select {
   471  
   472  	case b.filterUpdates <- filterUpdate{
   473  		newUtxos:     newUtxos,
   474  		updateHeight: updateHeight,
   475  	}:
   476  		return nil
   477  
   478  	case <-b.quit:
   479  		return fmt.Errorf("chain filter shutting down")
   480  	}
   481  }
   482  
   483  // FilteredBlocks returns the channel that filtered blocks are to be sent over.
   484  // Each time a block is connected to the end of a main chain, and appropriate
   485  // FilteredBlock which contains the transactions which mutate our watched UTXO
   486  // set is to be returned.
   487  //
   488  // NOTE: This is part of the FilteredChainView interface.
   489  func (b *DcrdFilteredChainView) FilteredBlocks() <-chan *FilteredBlock {
   490  	return b.blockQueue.newBlocks
   491  }
   492  
   493  // DisconnectedBlocks returns a receive only channel which will be sent upon
   494  // with the empty filtered blocks of blocks which are disconnected from the
   495  // main chain in the case of a re-org.
   496  //
   497  // NOTE: This is part of the FilteredChainView interface.
   498  func (b *DcrdFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock {
   499  	return b.blockQueue.staleBlocks
   500  }