block_filterer.go raw

   1  package chainclient
   2  
   3  import (
   4  	"github.com/p9c/p9/pkg/btcaddr"
   5  	"github.com/p9c/p9/pkg/chaincfg"
   6  	"github.com/p9c/p9/pkg/txscript"
   7  	"github.com/p9c/p9/pkg/util"
   8  	am "github.com/p9c/p9/pkg/waddrmgr"
   9  	"github.com/p9c/p9/pkg/wire"
  10  )
  11  
  12  // BlockFilterer is used to iteratively scan blocks for a set of addresses of interest. This is done by constructing
  13  // reverse indexes mapping the addresses to a ScopedIndex, which permits the reconstruction of the exact child
  14  // derivation paths that reported matches.
  15  //
  16  // Once initialized, a BlockFilterer can be used to scan any number of blocks until a invocation of `FilterBlock`
  17  // returns true. This allows the reverse indexes to be reused in the event that the set of addresses does not need to be
  18  // altered. After a match is reported, a new BlockFilterer should be initialized with the updated set of addresses that
  19  // include any new keys that are now within our look-ahead.
  20  //
  21  // We track internal and external addresses separately in order to conserve the amount of space occupied in memory.
  22  // Specifically, the account and branch combined contribute only 1-bit of information when using the default scopes used
  23  // by the wallet. Thus we can avoid storing an additional 64-bits per address of interest by not storing the full
  24  // derivation paths, and instead opting to allow the caller to contextually infer the account (DefaultAccount) and
  25  // branch (Internal or External).
  26  type BlockFilterer struct {
  27  	// Params specifies the chain netparams of the current network.
  28  	Params *chaincfg.Params
  29  	// ExReverseFilter holds a reverse index mapping an external address to the scoped index from which it was derived.
  30  	ExReverseFilter map[string]am.ScopedIndex
  31  	// InReverseFilter holds a reverse index mapping an internal address to the scoped index from which it was derived.
  32  	InReverseFilter map[string]am.ScopedIndex
  33  	// WatchedOutPoints is a global set of outpoints being tracked by the wallet. This allows the block filterer to
  34  	// check for spends from an outpoint we own.
  35  	WatchedOutPoints map[wire.OutPoint]btcaddr.Address
  36  	// FoundExternal is a two-layer map recording the scope and index of external addresses found in a single block.
  37  	FoundExternal map[am.KeyScope]map[uint32]struct{}
  38  	// FoundInternal is a two-layer map recording the scope and index of internal addresses found in a single block.
  39  	FoundInternal map[am.KeyScope]map[uint32]struct{}
  40  	// FoundOutPoints is a set of outpoints found in a single block whose address belongs to the wallet.
  41  	FoundOutPoints map[wire.OutPoint]btcaddr.Address
  42  	// RelevantTxns records the transactions found in a particular block that contained matches from an address in
  43  	// either ExReverseFilter or InReverseFilter.
  44  	RelevantTxns []*wire.MsgTx
  45  }
  46  
  47  // NewBlockFilterer constructs the reverse indexes for the current set of external and internal addresses that we are
  48  // searching for, and is used to scan successive blocks for addresses of interest. A particular block filter can be
  49  // reused until the first call from `FilterBlock` returns true.
  50  func NewBlockFilterer(
  51  	params *chaincfg.Params,
  52  	req *FilterBlocksRequest,
  53  ) *BlockFilterer {
  54  	// Construct a reverse index by address string for the requested external addresses.
  55  	nExAddrs := len(req.ExternalAddrs)
  56  	exReverseFilter := make(map[string]am.ScopedIndex, nExAddrs)
  57  	for scopedIndex, addr := range req.ExternalAddrs {
  58  		exReverseFilter[addr.EncodeAddress()] = scopedIndex
  59  	}
  60  	// Construct a reverse index by address string for the requested internal addresses.
  61  	nInAddrs := len(req.InternalAddrs)
  62  	inReverseFilter := make(map[string]am.ScopedIndex, nInAddrs)
  63  	for scopedIndex, addr := range req.InternalAddrs {
  64  		inReverseFilter[addr.EncodeAddress()] = scopedIndex
  65  	}
  66  	foundExternal := make(map[am.KeyScope]map[uint32]struct{})
  67  	foundInternal := make(map[am.KeyScope]map[uint32]struct{})
  68  	foundOutPoints := make(map[wire.OutPoint]btcaddr.Address)
  69  	return &BlockFilterer{
  70  		Params:           params,
  71  		ExReverseFilter:  exReverseFilter,
  72  		InReverseFilter:  inReverseFilter,
  73  		WatchedOutPoints: req.WatchedOutPoints,
  74  		FoundExternal:    foundExternal,
  75  		FoundInternal:    foundInternal,
  76  		FoundOutPoints:   foundOutPoints,
  77  	}
  78  }
  79  
  80  // FilterBlock parses all txns in the provided block, searching for any that contain addresses of interest in either the
  81  // external or internal reverse filters. This method return true iff the block contains a non-zero number of addresses
  82  // of interest, or a transaction in the block spends from outpoints controlled by the wallet.
  83  func (bf *BlockFilterer) FilterBlock(block *wire.Block) bool {
  84  	var hasRelevantTxns bool
  85  	for _, tx := range block.Transactions {
  86  		if bf.FilterTx(tx) {
  87  			bf.RelevantTxns = append(bf.RelevantTxns, tx)
  88  			hasRelevantTxns = true
  89  		}
  90  	}
  91  	return hasRelevantTxns
  92  }
  93  
  94  // FilterTx scans all txouts in the provided txn, testing to see if any found addresses match those contained within the
  95  // external or internal reverse indexes. This method returns true iff the txn contains a non-zero number of addresses of
  96  // interest, or the transaction spends from an outpoint that belongs to the wallet.
  97  func (bf *BlockFilterer) FilterTx(tx *wire.MsgTx) bool {
  98  	var isRelevant bool
  99  	// First, check the inputs to this transaction to see if they spend any inputs belonging to the wallet. In addition
 100  	// to checking WatchedOutPoints, we also check FoundOutPoints, in case a txn spends from an outpoint created in the
 101  	// same block.
 102  	for _, in := range tx.TxIn {
 103  		if _, ok := bf.WatchedOutPoints[in.PreviousOutPoint]; ok {
 104  			isRelevant = true
 105  		}
 106  		if _, ok := bf.FoundOutPoints[in.PreviousOutPoint]; ok {
 107  			isRelevant = true
 108  		}
 109  	}
 110  	// Now, parse all of the outputs created by this transactions, and see if they contain any addresses known the
 111  	// wallet using our reverse indexes for both external and internal addresses. If a new output is found, we will add
 112  	// the outpoint to our set of FoundOutPoints.
 113  	var e error
 114  	for i, out := range tx.TxOut {
 115  		var addrs []btcaddr.Address
 116  		_, addrs, _, e = txscript.ExtractPkScriptAddrs(
 117  			out.PkScript, bf.Params,
 118  		)
 119  		if e != nil {
 120  			W.F(
 121  				"could not parse output script in %s:%d: %v",
 122  				tx.TxHash(), i, e,
 123  			)
 124  			continue
 125  		}
 126  		if !bf.FilterOutputAddrs(addrs) {
 127  			continue
 128  		}
 129  		// If we've reached this point, then the output contains an address of interest.
 130  		isRelevant = true
 131  		// Record the outpoint that containing the address in our set of found outpoints, so that the caller can update
 132  		// its global set of watched outpoints.
 133  		outPoint := wire.OutPoint{
 134  			Hash:  *util.NewTx(tx).Hash(),
 135  			Index: uint32(i),
 136  		}
 137  		bf.FoundOutPoints[outPoint] = addrs[0]
 138  	}
 139  	return isRelevant
 140  }
 141  
 142  // FilterOutputAddrs tests the set of addresses against the block filterer's external and internal reverse address
 143  // indexes. If any are found, they are added to set of external and internal found addresses, respectively. This method
 144  // returns true iff a non-zero number of the provided addresses are of interest.
 145  func (bf *BlockFilterer) FilterOutputAddrs(addrs []btcaddr.Address) bool {
 146  	var isRelevant bool
 147  	for _, addr := range addrs {
 148  		addrStr := addr.EncodeAddress()
 149  		if scopedIndex, ok := bf.ExReverseFilter[addrStr]; ok {
 150  			bf.foundExternal(scopedIndex)
 151  			isRelevant = true
 152  		}
 153  		if scopedIndex, ok := bf.InReverseFilter[addrStr]; ok {
 154  			bf.foundInternal(scopedIndex)
 155  			isRelevant = true
 156  		}
 157  	}
 158  	return isRelevant
 159  }
 160  
 161  // foundExternal marks the scoped index as found within the block filterer's FoundExternal map. If this the first index
 162  // found for a particular scope, the scope's second layer map will be initialized before marking the index.
 163  func (bf *BlockFilterer) foundExternal(scopedIndex am.ScopedIndex) {
 164  	if _, ok := bf.FoundExternal[scopedIndex.Scope]; !ok {
 165  		bf.FoundExternal[scopedIndex.Scope] = make(map[uint32]struct{})
 166  	}
 167  	bf.FoundExternal[scopedIndex.Scope][scopedIndex.Index] = struct{}{}
 168  }
 169  
 170  // foundInternal marks the scoped index as found within the block filterer's FoundInternal map. If this the first index
 171  // found for a particular scope, the scope's second layer map will be initialized before marking the index.
 172  func (bf *BlockFilterer) foundInternal(scopedIndex am.ScopedIndex) {
 173  	if _, ok := bf.FoundInternal[scopedIndex.Scope]; !ok {
 174  		bf.FoundInternal[scopedIndex.Scope] = make(map[uint32]struct{})
 175  	}
 176  	bf.FoundInternal[scopedIndex.Scope][scopedIndex.Index] = struct{}{}
 177  }
 178