github.com/decred/dcrlnd@v0.7.6/routing/chainview/chainscan.go (about) 1 package chainview 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "sync/atomic" 8 9 "github.com/decred/dcrd/chaincfg/chainhash" 10 "github.com/decred/dcrd/gcs/v4" 11 "github.com/decred/dcrd/wire" 12 "github.com/decred/dcrlnd/chainscan" 13 "github.com/decred/dcrlnd/channeldb" 14 ) 15 16 type chainscanChainSource interface { 17 GetBlock(context.Context, *chainhash.Hash) (*wire.MsgBlock, error) 18 CurrentTip(context.Context) (*chainhash.Hash, int32, error) 19 20 ChainEvents(context.Context) <-chan chainscan.ChainEvent 21 22 GetCFilter(context.Context, int32) (*chainhash.Hash, [16]byte, *gcs.FilterV2, error) 23 24 GetBlockHash(context.Context, int32) (*chainhash.Hash, error) 25 GetBlockHeader(context.Context, *chainhash.Hash) (*wire.BlockHeader, error) 26 27 Run(context.Context) error 28 } 29 30 // chainscanFilteredChainView is an implementation of the FilteredChainView 31 // interface which uses the chainscan package to perform its duties. 32 type chainscanFilteredChainView struct { 33 started int32 // To be used atomically. 34 stopped int32 // To be used atomically. 35 36 // bestHeight is the height of the latest block added to the 37 // blockQueue from the onFilteredConnectedMethod. It is used to 38 // determine up to what height we would need to rescan in case 39 // of a filter update. 40 bestHeightMtx sync.Mutex 41 bestHeight int64 42 43 // blockEventQueue is the ordered queue used to keep the order 44 // of connected and disconnected blocks sent to the reader of the 45 // chainView. 46 blockQueue *blockEventQueue 47 48 // filterUpdates is a channel in which updates to the utxo filter 49 // attached to this instance are sent over. 50 filterUpdates chan csFilterUpdate 51 52 // chainFilter is the set of utox's that we're currently watching 53 // spends for within the chain. 54 // 55 // It stores in the value the cancelChan that once closed removes the 56 // specified outpoint from the tipWatcher. 57 filterMtx sync.RWMutex 58 chainFilter map[wire.OutPoint]chan struct{} 59 60 // filterBlockReqs is a channel in which requests to filter select 61 // blocks will be sent over. 62 filterBlockReqs chan *filterBlockReq 63 64 chainSource chainscanChainSource 65 tipWatcher *chainscan.TipWatcher 66 chainEvents <-chan chainscan.ChainEvent 67 tipWatcherTxs map[chainhash.Hash]map[*wire.MsgTx]chainscan.Event 68 69 ctx context.Context 70 cancelCtx func() 71 72 quit chan struct{} 73 wg sync.WaitGroup 74 } 75 76 // A compile time check to ensure chainscanFilteredChainView implements the 77 // chainview.FilteredChainView. 78 var _ FilteredChainView = (*chainscanFilteredChainView)(nil) 79 80 // newChainscanFilteredChainView creates a new instance of a FilteredChainView 81 // from a compatible chain source implementation. 82 func newChainscanFilteredChainView(cs chainscanChainSource) (*chainscanFilteredChainView, error) { 83 ctx, cancel := context.WithCancel(context.Background()) 84 85 tipWatcher := chainscan.NewTipWatcher(cs) 86 chainView := &chainscanFilteredChainView{ 87 chainFilter: make(map[wire.OutPoint]chan struct{}), 88 filterUpdates: make(chan csFilterUpdate), 89 filterBlockReqs: make(chan *filterBlockReq), 90 quit: make(chan struct{}), 91 blockQueue: newBlockEventQueue(), 92 93 chainSource: cs, 94 tipWatcher: tipWatcher, 95 chainEvents: tipWatcher.ChainEvents(ctx), 96 tipWatcherTxs: make(map[chainhash.Hash]map[*wire.MsgTx]chainscan.Event), 97 98 ctx: ctx, 99 cancelCtx: cancel, 100 } 101 102 return chainView, nil 103 } 104 105 func runAndLogOnError(ctx context.Context, f func(context.Context) error, name string) { 106 go func() { 107 err := f(ctx) 108 select { 109 case <-ctx.Done(): 110 // Any errs were due to done() so, ok 111 return 112 default: 113 } 114 if err != nil { 115 log.Errorf("CSFilteredView error while running %s: %v", name, err) 116 } 117 }() 118 } 119 120 // Start starts all goroutines necessary for normal operation. 121 // 122 // NOTE: This is part of the FilteredChainView interface. 123 func (b *chainscanFilteredChainView) Start() error { 124 // Already started? 125 if atomic.AddInt32(&b.started, 1) != 1 { 126 return nil 127 } 128 129 log.Infof("ChainscanFilteredChainView starting") 130 131 runAndLogOnError(b.ctx, b.chainSource.Run, "chainSource") 132 runAndLogOnError(b.ctx, b.tipWatcher.Run, "tipWatcher") 133 134 _, bestHeight, err := b.chainSource.CurrentTip(b.ctx) 135 if err != nil { 136 return err 137 } 138 139 b.bestHeightMtx.Lock() 140 b.bestHeight = int64(bestHeight) 141 b.bestHeightMtx.Unlock() 142 143 b.blockQueue.Start() 144 145 b.wg.Add(2) 146 go b.handleChainEvents() 147 go b.chainFilterer() 148 149 return nil 150 } 151 152 // Stop stops all goroutines which we launched by the prior call to the Start 153 // method. 154 // 155 // NOTE: This is part of the FilteredChainView interface. 156 func (b *chainscanFilteredChainView) Stop() error { 157 // Already shutting down? 158 if atomic.AddInt32(&b.stopped, 1) != 1 { 159 return nil 160 } 161 162 // Shutdown chainscan services. 163 b.cancelCtx() 164 165 b.blockQueue.Stop() 166 167 log.Infof("FilteredChainView stopping") 168 169 close(b.quit) 170 b.wg.Wait() 171 172 return nil 173 } 174 175 func (b *chainscanFilteredChainView) foundAtTip(e chainscan.Event, _ chainscan.FindFunc) { 176 log.Tracef("Found at tip bh %s: %s", e.BlockHash, e) 177 txs, ok := b.tipWatcherTxs[e.BlockHash] 178 if !ok { 179 txs = make(map[*wire.MsgTx]chainscan.Event) 180 b.tipWatcherTxs[e.BlockHash] = txs 181 } 182 txs[e.Tx] = e 183 } 184 185 // filterBlock filters the given block hash against a currently processed block 186 // by the tipWatcher. 187 // 188 // This removes any found spent utxos from the tipWatcher and the list of 189 // watched utxos. 190 func (b *chainscanFilteredChainView) filterBlock(bh *chainhash.Hash) []*wire.MsgTx { 191 matches := b.tipWatcherTxs[*bh] 192 delete(b.tipWatcherTxs, *bh) 193 txs := make([]*wire.MsgTx, 0, len(matches)) 194 b.filterMtx.Lock() 195 for _, m := range matches { 196 txs = append(txs, m.Tx) 197 outp := m.Tx.TxIn[m.Index].PreviousOutPoint 198 if cancelChan, ok := b.chainFilter[outp]; ok { 199 close(cancelChan) 200 delete(b.chainFilter, outp) 201 } 202 } 203 b.filterMtx.Unlock() 204 205 return txs 206 } 207 208 // onBlockConnected is called for each block that's connected to the end of the 209 // main chain. Based on our current chain filter, the block may or may not 210 // include any relevant transactions. 211 func (b *chainscanFilteredChainView) onBlockConnected(e chainscan.BlockConnectedEvent) { 212 txs := b.filterBlock(e.BlockHash()) 213 214 // We record the height of the last connected block added to the 215 // blockQueue such that we can scan up to this height in case of a 216 // rescan. It must be protected by a mutex since a filter update might 217 // be trying to read it concurrently. 218 b.bestHeightMtx.Lock() 219 b.bestHeight = int64(e.Height) 220 b.bestHeightMtx.Unlock() 221 222 block := &FilteredBlock{ 223 Hash: e.Hash, 224 Height: int64(e.Height), 225 Transactions: txs, 226 } 227 228 b.blockQueue.Add(&blockEvent{ 229 eventType: connected, 230 block: block, 231 }) 232 } 233 234 // onBlockDisconnected is a callback which is executed once a block is 235 // disconnected from the end of the main chain. 236 func (b *chainscanFilteredChainView) onBlockDisconnected(e chainscan.BlockDisconnectedEvent) { 237 log.Debugf("Got disconnected block at height %d: %s", e.Height, 238 e.Hash) 239 240 filteredBlock := &FilteredBlock{ 241 Hash: e.Hash, 242 Height: int64(e.Height), 243 } 244 245 b.blockQueue.Add(&blockEvent{ 246 eventType: disconnected, 247 block: filteredBlock, 248 }) 249 } 250 251 func (b *chainscanFilteredChainView) handleChainEvents() { 252 defer b.wg.Done() 253 254 for { 255 var ce chainscan.ChainEvent 256 select { 257 case <-b.ctx.Done(): 258 return 259 case ce = <-b.chainEvents: 260 } 261 262 switch e := ce.(type) { 263 case chainscan.BlockConnectedEvent: 264 b.onBlockConnected(e) 265 case chainscan.BlockDisconnectedEvent: 266 b.onBlockDisconnected(e) 267 default: 268 log.Warnf("Unknown block event: %t", ce) 269 } 270 } 271 } 272 273 // FilterBlock takes a block hash, and returns a FilteredBlocks which is the 274 // result of applying the current registered UTXO sub-set on the block 275 // corresponding to that block hash. If any watched UTOX's are spent by the 276 // selected lock, then the internal chainFilter will also be updated. 277 // 278 // NOTE: This is part of the FilteredChainView interface. 279 func (b *chainscanFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) { 280 req := &filterBlockReq{ 281 blockHash: blockHash, 282 resp: make(chan *FilteredBlock, 1), 283 err: make(chan error, 1), 284 } 285 286 select { 287 case b.filterBlockReqs <- req: 288 case <-b.quit: 289 return nil, fmt.Errorf("FilteredChainView shutting down") 290 } 291 292 return <-req.resp, <-req.err 293 } 294 295 func (b *chainscanFilteredChainView) rescanBlock(bh *chainhash.Hash) ([]*wire.MsgTx, int32, error) { 296 header, err := b.chainSource.GetBlockHeader(b.ctx, bh) 297 if err != nil { 298 return nil, 0, err 299 } 300 301 bh, cfkey, filter, err := b.chainSource.GetCFilter(b.ctx, int32(header.Height)) 302 if err != nil { 303 return nil, 0, err 304 } 305 306 e := chainscan.BlockConnectedEvent{ 307 Height: int32(header.Height), 308 Hash: *bh, 309 CFKey: cfkey, 310 Filter: filter, 311 PrevHash: header.PrevBlock, 312 } 313 314 if err := b.tipWatcher.ForceRescan(b.ctx, &e); err != nil { 315 return nil, 0, err 316 } 317 318 return b.filterBlock(bh), int32(header.Height), nil 319 } 320 321 func (b *chainscanFilteredChainView) forceRescan(updateHeight, bestHeight int64) error { 322 // Track the previous block hash to fill in the data. 323 prevHash, err := b.chainSource.GetBlockHash(b.ctx, int32(updateHeight)) 324 if err != nil { 325 return err 326 } 327 328 for height := updateHeight + 1; height < bestHeight+1; height++ { 329 bh, cfkey, filter, err := b.chainSource.GetCFilter(b.ctx, int32(height)) 330 if err != nil { 331 return err 332 } 333 334 e := chainscan.BlockConnectedEvent{ 335 Height: int32(height), 336 Hash: *bh, 337 CFKey: cfkey, 338 Filter: filter, 339 PrevHash: *prevHash, 340 } 341 prevHash = bh 342 343 if err := b.tipWatcher.ForceRescan(b.ctx, &e); err != nil { 344 log.Warnf("Unable to rescan block "+ 345 "with hash %s at height %d: %v", 346 bh, height, err) 347 continue 348 } 349 350 b.onBlockConnected(e) 351 } 352 353 return nil 354 } 355 356 // chainFilterer is the primary goroutine which: listens for new blocks coming 357 // and dispatches the relevant FilteredBlock notifications, updates the filter 358 // due to requests by callers, and finally is able to preform targeted block 359 // filtration. 360 // 361 // TODO(roasbeef): change to use loadfilter RPC's 362 func (b *chainscanFilteredChainView) chainFilterer() { 363 defer b.wg.Done() 364 365 for { 366 select { 367 // The caller has just sent an update to the current chain 368 // filter, so we'll apply the update, possibly rewinding our 369 // state partially. 370 case update := <-b.filterUpdates: 371 372 // First, we'll add all the new UTXO's to the set of 373 // watched UTXO's, eliminating any duplicates in the 374 // process. 375 log.Tracef("Updating chain filter with new UTXO's: %v", 376 newLogClosure(func() string { 377 var s string 378 for _, u := range update.newUtxos { 379 s = s + u.OutPoint.String() + " " 380 } 381 return s 382 }), 383 ) 384 385 // All blocks gotten after we loaded the filter will 386 // have the filter applied, but we will need to rescan 387 // the blocks up to the height of the block we last 388 // added to the blockQueue. 389 b.bestHeightMtx.Lock() 390 bestHeight := b.bestHeight 391 b.bestHeightMtx.Unlock() 392 393 cancelChans := make(map[wire.OutPoint]chan struct{}, len(update.newUtxos)) 394 b.filterMtx.Lock() 395 for _, newOp := range update.newUtxos { 396 // Ignore if we already watch this. 397 if _, ok := b.chainFilter[newOp.OutPoint]; ok { 398 continue 399 } 400 401 cancelChan := make(chan struct{}) 402 b.chainFilter[newOp.OutPoint] = cancelChan 403 cancelChans[newOp.OutPoint] = cancelChan 404 } 405 b.filterMtx.Unlock() 406 407 // Add the new utxos as targets for out instance of the 408 // tip watcher. 409 for _, newOp := range update.newUtxos { 410 cancelChan := cancelChans[newOp.OutPoint] 411 scriptVersion := uint16(0) 412 swhChan := make(chan int32) 413 target := chainscan.SpentOutPoint( 414 newOp.OutPoint, 415 scriptVersion, 416 newOp.FundingPkScript, 417 ) 418 b.tipWatcher.Find( 419 target, 420 chainscan.WithFoundCallback(b.foundAtTip), 421 chainscan.WithCancelChan(cancelChan), 422 chainscan.WithStartWatchHeightChan(swhChan), 423 ) 424 425 // Wait until we know the tipWatcher has 426 // started watching. 427 select { 428 case <-swhChan: 429 case <-b.quit: 430 return 431 } 432 } 433 434 // If the update height matches our best known height, 435 // then we don't need to do any rewinding. 436 if update.updateHeight == bestHeight { 437 continue 438 } 439 440 // Otherwise, we'll rewind the state to ensure the 441 // caller doesn't miss any relevant notifications. 442 err := b.forceRescan(update.updateHeight, bestHeight) 443 if err != nil { 444 log.Errorf("Error forcing utxo rescan on range (%d,%d]: %v", 445 update.updateHeight, bestHeight, err) 446 } 447 448 // We've received a new request to manually filter a block. 449 case req := <-b.filterBlockReqs: 450 txs, height, err := b.rescanBlock(req.blockHash) 451 if err != nil { 452 req.err <- err 453 req.resp <- nil 454 continue 455 } 456 457 // Once we have this info, we can directly filter the 458 // block and dispatch the proper notification. 459 req.resp <- &FilteredBlock{ 460 Hash: *req.blockHash, 461 Height: int64(height), 462 Transactions: txs, 463 } 464 req.err <- err 465 466 case <-b.quit: 467 return 468 } 469 } 470 } 471 472 // csFilterUpdate is a message sent to the chainFilterer to update the current 473 // chainFilter state. 474 type csFilterUpdate struct { 475 newUtxos []channeldb.EdgePoint 476 updateHeight int64 477 } 478 479 // UpdateFilter updates the UTXO filter which is to be consulted when creating 480 // FilteredBlocks to be sent to subscribed clients. This method is cumulative 481 // meaning repeated calls to this method should _expand_ the size of the UTXO 482 // sub-set currently being watched. If the set updateHeight is _lower_ than 483 // the best known height of the implementation, then the state should be 484 // rewound to ensure all relevant notifications are dispatched. 485 // 486 // NOTE: This is part of the FilteredChainView interface. 487 func (b *chainscanFilteredChainView) UpdateFilter(ops []channeldb.EdgePoint, 488 updateHeight int64) error { 489 490 // Make a copy to avoid having this changed under our feet. 491 newUtxos := make([]channeldb.EdgePoint, len(ops)) 492 copy(newUtxos, ops) 493 494 select { 495 496 case b.filterUpdates <- csFilterUpdate{ 497 newUtxos: newUtxos, 498 updateHeight: updateHeight, 499 }: 500 return nil 501 502 case <-b.quit: 503 return fmt.Errorf("chain filter shutting down") 504 } 505 } 506 507 // FilteredBlocks returns the channel that filtered blocks are to be sent over. 508 // Each time a block is connected to the end of a main chain, and appropriate 509 // FilteredBlock which contains the transactions which mutate our watched UTXO 510 // set is to be returned. 511 // 512 // NOTE: This is part of the FilteredChainView interface. 513 func (b *chainscanFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { 514 return b.blockQueue.newBlocks 515 } 516 517 // DisconnectedBlocks returns a receive only channel which will be sent upon 518 // with the empty filtered blocks of blocks which are disconnected from the 519 // main chain in the case of a re-org. 520 // 521 // NOTE: This is part of the FilteredChainView interface. 522 func (b *chainscanFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { 523 return b.blockQueue.staleBlocks 524 }