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