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 }