github.com/decred/dcrlnd@v0.7.6/routing/chainview/dcrd.go (about) 1 package chainview 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "fmt" 8 "sync" 9 "sync/atomic" 10 11 "github.com/decred/dcrd/chaincfg/chainhash" 12 jsontypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 13 "github.com/decred/dcrd/rpcclient/v8" 14 "github.com/decred/dcrd/wire" 15 "github.com/decred/dcrlnd/channeldb" 16 ) 17 18 // DcrdFilteredChainView is an implementation of the FilteredChainView 19 // interface which is backed by an active websockets connection to dcrd. 20 type DcrdFilteredChainView struct { 21 started int32 // To be used atomically. 22 stopped int32 // To be used atomically. 23 24 // bestHeight is the height of the latest block added to the 25 // blockQueue from the onFilteredConnectedMethod. It is used to 26 // determine up to what height we would need to rescan in case 27 // of a filter update. 28 bestHeightMtx sync.Mutex 29 bestHeight int64 30 31 dcrdConn *rpcclient.Client 32 33 // blockEventQueue is the ordered queue used to keep the order 34 // of connected and disconnected blocks sent to the reader of the 35 // chainView. 36 blockQueue *blockEventQueue 37 38 // filterUpdates is a channel in which updates to the utxo filter 39 // attached to this instance are sent over. 40 filterUpdates chan filterUpdate 41 42 // chainFilter is the set of utox's that we're currently watching 43 // spends for within the chain. 44 filterMtx sync.RWMutex 45 chainFilter map[wire.OutPoint]struct{} 46 47 // filterBlockReqs is a channel in which requests to filter select 48 // blocks will be sent over. 49 filterBlockReqs chan *filterBlockReq 50 51 quit chan struct{} 52 wg sync.WaitGroup 53 } 54 55 // A compile time check to ensure DcrdFilteredChainView implements the 56 // chainview.FilteredChainView. 57 var _ FilteredChainView = (*DcrdFilteredChainView)(nil) 58 59 // NewDcrdFilteredChainView creates a new instance of a FilteredChainView from 60 // RPC credentials for an active dcrd instance. 61 func NewDcrdFilteredChainView(config rpcclient.ConnConfig) (*DcrdFilteredChainView, error) { 62 chainView := &DcrdFilteredChainView{ 63 chainFilter: make(map[wire.OutPoint]struct{}), 64 filterUpdates: make(chan filterUpdate), 65 filterBlockReqs: make(chan *filterBlockReq), 66 quit: make(chan struct{}), 67 } 68 69 ntfnCallbacks := &rpcclient.NotificationHandlers{ 70 OnBlockConnected: chainView.onBlockConnected, 71 OnBlockDisconnected: chainView.onBlockDisconnected, 72 } 73 74 // Disable connecting to dcrd within the rpcclient.New method. We 75 // defer establishing the connection to our .Start() method. 76 config.DisableConnectOnNew = true 77 config.DisableAutoReconnect = false 78 chainConn, err := rpcclient.New(&config, ntfnCallbacks) 79 if err != nil { 80 return nil, err 81 } 82 chainView.dcrdConn = chainConn 83 84 chainView.blockQueue = newBlockEventQueue() 85 86 return chainView, nil 87 } 88 89 // Start starts all goroutines necessary for normal operation. 90 // 91 // NOTE: This is part of the FilteredChainView interface. 92 func (b *DcrdFilteredChainView) Start() error { 93 // Already started? 94 if atomic.AddInt32(&b.started, 1) != 1 { 95 return nil 96 } 97 98 log.Infof("FilteredChainView starting") 99 100 // Connect to dcrd, and register for notifications on connected, and 101 // disconnected blocks. 102 if err := b.dcrdConn.Connect(context.Background(), true); err != nil { 103 return err 104 } 105 if err := b.dcrdConn.NotifyBlocks(context.TODO()); err != nil { 106 return err 107 } 108 109 _, bestHeight, err := b.dcrdConn.GetBestBlock(context.TODO()) 110 if err != nil { 111 return err 112 } 113 114 b.bestHeightMtx.Lock() 115 b.bestHeight = bestHeight 116 b.bestHeightMtx.Unlock() 117 118 b.blockQueue.Start() 119 120 b.wg.Add(1) 121 go b.chainFilterer() 122 123 return nil 124 } 125 126 // Stop stops all goroutines which we launched by the prior call to the Start 127 // method. 128 // 129 // NOTE: This is part of the FilteredChainView interface. 130 func (b *DcrdFilteredChainView) Stop() error { 131 // Already shutting down? 132 if atomic.AddInt32(&b.stopped, 1) != 1 { 133 return nil 134 } 135 136 // Shutdown the rpc client, this gracefully disconnects from dcrd, and 137 // cleans up all related resources. 138 b.dcrdConn.Shutdown() 139 140 b.blockQueue.Stop() 141 142 log.Infof("FilteredChainView stopping") 143 144 close(b.quit) 145 b.wg.Wait() 146 147 return nil 148 } 149 150 // onBlockConnected is called for each block that's connected to the end of the 151 // main chain. Based on our current chain filter, the block may or may not 152 // include any relevant transactions. 153 func (b *DcrdFilteredChainView) onBlockConnected(blockHeader []byte, txns [][]byte) { 154 var header wire.BlockHeader 155 if err := header.FromBytes(blockHeader); err != nil { 156 log.Warnf("Received block connected with malformed header: %v", err) 157 return 158 } 159 160 mtxs := make([]*wire.MsgTx, len(txns)) 161 b.filterMtx.Lock() 162 for i, txBytes := range txns { 163 var mtx wire.MsgTx 164 if err := mtx.FromBytes(txBytes); err != nil { 165 log.Warnf("Received block connected with malformed tx: %v", err) 166 return 167 } 168 mtxs[i] = &mtx 169 } 170 171 for _, mtx := range mtxs { 172 for _, txIn := range mtx.TxIn { 173 // We can delete this outpoint from the chainFilter, as 174 // we just received a block where it was spent. In case 175 // of a reorg, this outpoint might get "un-spent", but 176 // that's okay since it would never be wise to consider 177 // the channel open again (since a spending transaction 178 // exists on the network). 179 delete(b.chainFilter, txIn.PreviousOutPoint) 180 } 181 } 182 b.filterMtx.Unlock() 183 184 // We record the height of the last connected block added to the 185 // blockQueue such that we can scan up to this height in case of 186 // a rescan. It must be protected by a mutex since a filter update 187 // might be trying to read it concurrently. 188 b.bestHeightMtx.Lock() 189 b.bestHeight = int64(header.Height) 190 b.bestHeightMtx.Unlock() 191 192 block := &FilteredBlock{ 193 Hash: header.BlockHash(), 194 Height: int64(header.Height), 195 Transactions: mtxs, 196 } 197 198 b.blockQueue.Add(&blockEvent{ 199 eventType: connected, 200 block: block, 201 }) 202 203 } 204 205 // onBlockDisconnected is a callback which is executed once a block is 206 // disconnected from the end of the main chain. 207 func (b *DcrdFilteredChainView) onBlockDisconnected(blockHeader []byte) { 208 var header wire.BlockHeader 209 if err := header.FromBytes(blockHeader); err != nil { 210 log.Warnf("Received block disconnected with malformed header: %v", err) 211 return 212 } 213 214 log.Debugf("got disconnected block at height %d: %v", header.Height, 215 header.BlockHash()) 216 217 filteredBlock := &FilteredBlock{ 218 Hash: header.BlockHash(), 219 Height: int64(header.Height), 220 } 221 222 b.blockQueue.Add(&blockEvent{ 223 eventType: disconnected, 224 block: filteredBlock, 225 }) 226 } 227 228 // filterBlockReq houses a request to manually filter a block specified by 229 // block hash. 230 type filterBlockReq struct { 231 blockHash *chainhash.Hash 232 resp chan *FilteredBlock 233 err chan error 234 } 235 236 // FilterBlock takes a block hash, and returns a FilteredBlocks which is the 237 // result of applying the current registered UTXO sub-set on the block 238 // corresponding to that block hash. If any watched UTOX's are spent by the 239 // selected lock, then the internal chainFilter will also be updated. 240 // 241 // NOTE: This is part of the FilteredChainView interface. 242 func (b *DcrdFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) { 243 req := &filterBlockReq{ 244 blockHash: blockHash, 245 resp: make(chan *FilteredBlock, 1), 246 err: make(chan error, 1), 247 } 248 249 select { 250 case b.filterBlockReqs <- req: 251 case <-b.quit: 252 return nil, fmt.Errorf("FilteredChainView shutting down") 253 } 254 255 return <-req.resp, <-req.err 256 } 257 258 // chainFilterer is the primary goroutine which: listens for new blocks coming 259 // and dispatches the relevant FilteredBlock notifications, updates the filter 260 // due to requests by callers, and finally is able to preform targeted block 261 // filtration. 262 // 263 // TODO(roasbeef): change to use loadfilter RPC's 264 func (b *DcrdFilteredChainView) chainFilterer() { 265 defer b.wg.Done() 266 267 // filterBlock is a helper function that scans the given block, and 268 // notes which transactions spend outputs which are currently being 269 // watched. Additionally, the chain filter will also be updated by 270 // removing any spent outputs. 271 filterBlock := func(blk *wire.MsgBlock) []*wire.MsgTx { 272 b.filterMtx.Lock() 273 defer b.filterMtx.Unlock() 274 275 var filteredTxns []*wire.MsgTx 276 for _, tx := range blk.Transactions { 277 var txAlreadyFiltered bool 278 for _, txIn := range tx.TxIn { 279 prevOp := txIn.PreviousOutPoint 280 if _, ok := b.chainFilter[prevOp]; !ok { 281 continue 282 } 283 284 delete(b.chainFilter, prevOp) 285 286 // Only add this txn to our list of filtered 287 // txns if it is the first previous outpoint to 288 // cause a match. 289 if txAlreadyFiltered { 290 continue 291 } 292 293 filteredTxns = append(filteredTxns, tx) 294 txAlreadyFiltered = true 295 296 } 297 } 298 299 return filteredTxns 300 } 301 302 decodeJSONBlock := func(block *jsontypes.RescannedBlock, 303 height int64) (*FilteredBlock, error) { 304 hash, err := chainhash.NewHashFromStr(block.Hash) 305 if err != nil { 306 return nil, err 307 } 308 txs := make([]*wire.MsgTx, 0, len(block.Transactions)) 309 for _, str := range block.Transactions { 310 b, err := hex.DecodeString(str) 311 if err != nil { 312 return nil, err 313 } 314 tx := &wire.MsgTx{} 315 err = tx.Deserialize(bytes.NewReader(b)) 316 if err != nil { 317 return nil, err 318 } 319 txs = append(txs, tx) 320 } 321 return &FilteredBlock{ 322 Hash: *hash, 323 Height: height, 324 Transactions: txs, 325 }, nil 326 } 327 328 for { 329 select { 330 // The caller has just sent an update to the current chain 331 // filter, so we'll apply the update, possibly rewinding our 332 // state partially. 333 case update := <-b.filterUpdates: 334 335 // First, we'll add all the new UTXO's to the set of 336 // watched UTXO's, eliminating any duplicates in the 337 // process. 338 log.Tracef("Updating chain filter with new UTXO's: %v", 339 update.newUtxos) 340 341 b.filterMtx.Lock() 342 for _, newOp := range update.newUtxos { 343 b.chainFilter[newOp] = struct{}{} 344 } 345 b.filterMtx.Unlock() 346 347 // Apply the new TX filter to dcrd, which will cause 348 // all following notifications from and calls to it 349 // return blocks filtered with the new filter. 350 b.dcrdConn.LoadTxFilter(context.TODO(), false, nil, update.newUtxos) 351 352 // All blocks gotten after we loaded the filter will 353 // have the filter applied, but we will need to rescan 354 // the blocks up to the height of the block we last 355 // added to the blockQueue. 356 b.bestHeightMtx.Lock() 357 bestHeight := b.bestHeight 358 b.bestHeightMtx.Unlock() 359 360 // If the update height matches our best known height, 361 // then we don't need to do any rewinding. 362 if update.updateHeight == bestHeight { 363 continue 364 } 365 366 // Otherwise, we'll rewind the state to ensure the 367 // caller doesn't miss any relevant notifications. 368 // Starting from the height _after_ the update height, 369 // we'll walk forwards, rescanning one block at a time 370 // with dcrd applying the newly loaded filter to each 371 // block. 372 for i := update.updateHeight + 1; i < bestHeight+1; i++ { 373 blockHash, err := b.dcrdConn.GetBlockHash(context.TODO(), i) 374 if err != nil { 375 log.Warnf("Unable to get block hash "+ 376 "for block at height %d: %v", 377 i, err) 378 continue 379 } 380 381 // To avoid dealing with the case where a reorg 382 // is happening while we rescan, we scan one 383 // block at a time, skipping blocks that might 384 // have gone missing. 385 rescanned, err := b.dcrdConn.Rescan( 386 context.TODO(), []chainhash.Hash{*blockHash}) 387 if err != nil { 388 log.Warnf("Unable to rescan block "+ 389 "with hash %v at height %d: %v", 390 blockHash, i, err) 391 continue 392 } 393 394 // If no block was returned from the rescan, it 395 // means no matching transactions were found. 396 if len(rescanned.DiscoveredData) != 1 { 397 log.Tracef("rescan of block %v at "+ 398 "height=%d yielded no "+ 399 "transactions", blockHash, i) 400 continue 401 } 402 decoded, err := decodeJSONBlock( 403 &rescanned.DiscoveredData[0], i) 404 if err != nil { 405 log.Errorf("Unable to decode block: %v", 406 err) 407 continue 408 } 409 b.blockQueue.Add(&blockEvent{ 410 eventType: connected, 411 block: decoded, 412 }) 413 } 414 415 // We've received a new request to manually filter a block. 416 case req := <-b.filterBlockReqs: 417 // First we'll fetch the block itself as well as some 418 // additional information including its height. 419 block, err := b.dcrdConn.GetBlock(context.TODO(), req.blockHash) 420 if err != nil { 421 req.err <- err 422 req.resp <- nil 423 continue 424 } 425 header, err := b.dcrdConn.GetBlockHeaderVerbose(context.TODO(), req.blockHash) 426 if err != nil { 427 req.err <- err 428 req.resp <- nil 429 continue 430 } 431 432 // Once we have this info, we can directly filter the 433 // block and dispatch the proper notification. 434 req.resp <- &FilteredBlock{ 435 Hash: *req.blockHash, 436 Height: int64(header.Height), 437 Transactions: filterBlock(block), 438 } 439 req.err <- err 440 441 case <-b.quit: 442 return 443 } 444 } 445 } 446 447 // filterUpdate is a message sent to the chainFilterer to update the current 448 // chainFilter state. 449 type filterUpdate struct { 450 newUtxos []wire.OutPoint 451 updateHeight int64 452 } 453 454 // UpdateFilter updates the UTXO filter which is to be consulted when creating 455 // FilteredBlocks to be sent to subscribed clients. This method is cumulative 456 // meaning repeated calls to this method should _expand_ the size of the UTXO 457 // sub-set currently being watched. If the set updateHeight is _lower_ than 458 // the best known height of the implementation, then the state should be 459 // rewound to ensure all relevant notifications are dispatched. 460 // 461 // NOTE: This is part of the FilteredChainView interface. 462 func (b *DcrdFilteredChainView) UpdateFilter(ops []channeldb.EdgePoint, 463 updateHeight int64) error { 464 465 newUtxos := make([]wire.OutPoint, len(ops)) 466 for i, op := range ops { 467 newUtxos[i] = op.OutPoint 468 } 469 470 select { 471 472 case b.filterUpdates <- filterUpdate{ 473 newUtxos: newUtxos, 474 updateHeight: updateHeight, 475 }: 476 return nil 477 478 case <-b.quit: 479 return fmt.Errorf("chain filter shutting down") 480 } 481 } 482 483 // FilteredBlocks returns the channel that filtered blocks are to be sent over. 484 // Each time a block is connected to the end of a main chain, and appropriate 485 // FilteredBlock which contains the transactions which mutate our watched UTXO 486 // set is to be returned. 487 // 488 // NOTE: This is part of the FilteredChainView interface. 489 func (b *DcrdFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { 490 return b.blockQueue.newBlocks 491 } 492 493 // DisconnectedBlocks returns a receive only channel which will be sent upon 494 // with the empty filtered blocks of blocks which are disconnected from the 495 // main chain in the case of a re-org. 496 // 497 // NOTE: This is part of the FilteredChainView interface. 498 func (b *DcrdFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { 499 return b.blockQueue.staleBlocks 500 }