From 2ff809fd78a908113a1aa0d1c62886139515fa98 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 23 Mar 2018 20:05:53 -0700 Subject: [PATCH] chain/block_filterer: adds BlockFilterer for scanning during recovery --- chain/block_filterer.go | 217 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 chain/block_filterer.go diff --git a/chain/block_filterer.go b/chain/block_filterer.go new file mode 100644 index 0000000..ee99237 --- /dev/null +++ b/chain/block_filterer.go @@ -0,0 +1,217 @@ +package chain + +import ( + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/waddrmgr" +) + +// BlockFilterer is used to iteratively scan blocks for a set of addresses of +// interest. This is done by constructing reverse indexes mapping the +// addresses to a ScopedIndex, which permits the reconstruction of the exact +// child deriviation paths that reported matches. +// +// Once initialized, a BlockFilterer can be used to scan any number of blocks +// until a invocation of `FilterBlock` returns true. This allows the reverse +// indexes to be resused in the event that the set of addresses does not need to +// be altered. After a match is reported, a new BlockFilterer should be +// initialized with the updated set of addresses that include any new keys that +// are now within our look-ahead. +// +// We track internal and external addresses separately in order to conserve the +// amount of space occupied in memory. Specifically, the account and branch +// combined contribute only 1-bit of information when using the default scopes +// used by the wallet. Thus we can avoid storing an additional 64-bits per +// address of interest by not storing the full derivation paths, and instead +// opting to allow the caller to contextually infer the account (DefaultAccount) +// and branch (Internal or External). +type BlockFilterer struct { + // Params specifies the chain params of the current network. + Params *chaincfg.Params + + // ExReverseFilter holds a reverse index mapping an external address to + // the scoped index from which it was derived. + ExReverseFilter map[string]waddrmgr.ScopedIndex + + // InReverseFilter holds a reverse index mapping an internal address to + // the scoped index from which it was derived. + InReverseFilter map[string]waddrmgr.ScopedIndex + + // WathcedOutPoints is a global set of outpoints being tracked by the + // wallet. This allows the block filterer to check for spends from an + // outpoint we own. + WatchedOutPoints map[wire.OutPoint]struct{} + + // FoundExternal is a two-layer map recording the scope and index of + // external addresses found in a single block. + FoundExternal map[waddrmgr.KeyScope]map[uint32]struct{} + + // FoundInternal is a two-layer map recording the scope and index of + // internal addresses found in a single block. + FoundInternal map[waddrmgr.KeyScope]map[uint32]struct{} + + // FoundOutPoints is a set of outpoints found in a single block whose + // address belongs to the wallet. + FoundOutPoints map[wire.OutPoint]struct{} + + // RelevantTxns records the transactions found in a particular block + // that contained matches from an address in either ExReverseFilter or + // InReverseFilter. + RelevantTxns []*wire.MsgTx +} + +// NewBlockFilterer constructs the reverse indexes for the current set of +// external and internal addresses that we are searching for, and is used to +// scan successive blocks for addresses of interest. A particular block filter +// can be reused until the first call from `FitlerBlock` returns true. +func NewBlockFilterer(params *chaincfg.Params, + req *FilterBlocksRequest) *BlockFilterer { + + // Construct a reverse index by address string for the requested + // external addresses. + nExAddrs := len(req.ExternalAddrs) + exReverseFilter := make(map[string]waddrmgr.ScopedIndex, nExAddrs) + for scopedIndex, addr := range req.ExternalAddrs { + exReverseFilter[addr.EncodeAddress()] = scopedIndex + } + + // Construct a reverse index by address string for the requested + // internal addresses. + nInAddrs := len(req.InternalAddrs) + inReverseFilter := make(map[string]waddrmgr.ScopedIndex, nInAddrs) + for scopedIndex, addr := range req.InternalAddrs { + inReverseFilter[addr.EncodeAddress()] = scopedIndex + } + + foundExternal := make(map[waddrmgr.KeyScope]map[uint32]struct{}) + foundInternal := make(map[waddrmgr.KeyScope]map[uint32]struct{}) + foundOutPoints := make(map[wire.OutPoint]struct{}) + + return &BlockFilterer{ + Params: params, + ExReverseFilter: exReverseFilter, + InReverseFilter: inReverseFilter, + WatchedOutPoints: req.WatchedOutPoints, + FoundExternal: foundExternal, + FoundInternal: foundInternal, + FoundOutPoints: foundOutPoints, + } +} + +// FilterBlock parses all txns in the provided block, searching for any that +// contain addresses of interest in either the external or internal reverse +// filters. This method return true iff the block contains a non-zero number of +// addresses of interest, or a transaction in the block spends from outpoints +// controlled by the wallet. +func (bf *BlockFilterer) FilterBlock(block *wire.MsgBlock) bool { + var hasRelevantTxns bool + for _, tx := range block.Transactions { + if bf.FilterTx(tx) { + bf.RelevantTxns = append(bf.RelevantTxns, tx) + hasRelevantTxns = true + } + } + + return hasRelevantTxns +} + +// FilterTx scans all txouts in the provided txn, testing to see if any found +// addresses match those contained within the external or internal reverse +// indexes. This method returns true iff the txn contains a non-zero number of +// addresses of interest, or the transaction spends from an outpoint that +// belongs to the wallet. +func (bf *BlockFilterer) FilterTx(tx *wire.MsgTx) bool { + var isRelevant bool + + // First, check the inputs to this transaction to see if they spend any + // inputs belonging to the wallet. In addition to checking + // WatchedOutPoints, we also check FoundOutPoints, in case a txn spends + // from an outpoint created in the same block. + for _, in := range tx.TxIn { + if _, ok := bf.WatchedOutPoints[in.PreviousOutPoint]; ok { + isRelevant = true + } + if _, ok := bf.FoundOutPoints[in.PreviousOutPoint]; ok { + isRelevant = true + } + } + + // Now, parse all of the outputs created by this transactions, and see + // if they contain any addresses known the wallet using our reverse + // indexes for both external and internal addresses. If a new output is + // found, we will add the outpoint to our set of FoundOutPoints. + for i, out := range tx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + out.PkScript, bf.Params, + ) + if err != nil { + log.Warnf("Could not parse output script in %s:%d: %v", + tx.TxHash(), i, err) + continue + } + + if !bf.FilterOutputAddrs(addrs) { + continue + } + + // If we've reached this point, then the output contains an + // address of interest. + isRelevant = true + + // Record the outpoint that containing the address in our set of + // found outpoints, so that the caller can update its global + // set of watched outpoints. + outPoint := wire.OutPoint{ + Hash: *btcutil.NewTx(tx).Hash(), + Index: uint32(i), + } + + bf.FoundOutPoints[outPoint] = struct{}{} + } + + return isRelevant +} + +// FilterOutputAddrs tests the set of addresses against the block filterer's +// external and internal reverse address indexes. If any are found, they are +// added to set of external and internal found addresses, respectively. This +// method returns true iff a non-zero number of the provided addresses are of +// interest. +func (bf *BlockFilterer) FilterOutputAddrs(addrs []btcutil.Address) bool { + var isRelevant bool + for _, addr := range addrs { + addrStr := addr.EncodeAddress() + if scopedIndex, ok := bf.ExReverseFilter[addrStr]; ok { + bf.foundExternal(scopedIndex) + isRelevant = true + } + if scopedIndex, ok := bf.InReverseFilter[addrStr]; ok { + bf.foundInternal(scopedIndex) + isRelevant = true + } + } + + return isRelevant +} + +// foundExternal marks the scoped index as found within the block filterer's +// FoundExternal map. If this the first index found for a particular scope, the +// scope's second layer map will be initialized before marking the index. +func (bf *BlockFilterer) foundExternal(scopedIndex waddrmgr.ScopedIndex) { + if _, ok := bf.FoundExternal[scopedIndex.Scope]; !ok { + bf.FoundExternal[scopedIndex.Scope] = make(map[uint32]struct{}) + } + bf.FoundExternal[scopedIndex.Scope][scopedIndex.Index] = struct{}{} +} + +// foundInternal marks the scoped index as found within the block filterer's +// FoundInternal map. If this the first index found for a particular scope, the +// scope's second layer map will be initialized before marking the index. +func (bf *BlockFilterer) foundInternal(scopedIndex waddrmgr.ScopedIndex) { + if _, ok := bf.FoundInternal[scopedIndex.Scope]; !ok { + bf.FoundInternal[scopedIndex.Scope] = make(map[uint32]struct{}) + } + bf.FoundInternal[scopedIndex.Scope][scopedIndex.Index] = struct{}{} +}