github.com/lbryio/lbcd@v0.22.119/blockchain/indexers/manager.go (about) 1 // Copyright (c) 2016 The btcsuite developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package indexers 6 7 import ( 8 "bytes" 9 "fmt" 10 11 "github.com/lbryio/lbcd/blockchain" 12 "github.com/lbryio/lbcd/chaincfg/chainhash" 13 "github.com/lbryio/lbcd/database" 14 "github.com/lbryio/lbcd/wire" 15 btcutil "github.com/lbryio/lbcutil" 16 ) 17 18 var ( 19 // indexTipsBucketName is the name of the db bucket used to house the 20 // current tip of each index. 21 indexTipsBucketName = []byte("idxtips") 22 ) 23 24 // ----------------------------------------------------------------------------- 25 // The index manager tracks the current tip of each index by using a parent 26 // bucket that contains an entry for index. 27 // 28 // The serialized format for an index tip is: 29 // 30 // [<block hash><block height>],... 31 // 32 // Field Type Size 33 // block hash chainhash.Hash chainhash.HashSize 34 // block height uint32 4 bytes 35 // ----------------------------------------------------------------------------- 36 37 // dbPutIndexerTip uses an existing database transaction to update or add the 38 // current tip for the given index to the provided values. 39 func dbPutIndexerTip(dbTx database.Tx, idxKey []byte, hash *chainhash.Hash, height int32) error { 40 serialized := make([]byte, chainhash.HashSize+4) 41 copy(serialized, hash[:]) 42 byteOrder.PutUint32(serialized[chainhash.HashSize:], uint32(height)) 43 44 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 45 return indexesBucket.Put(idxKey, serialized) 46 } 47 48 // dbFetchIndexerTip uses an existing database transaction to retrieve the 49 // hash and height of the current tip for the provided index. 50 func dbFetchIndexerTip(dbTx database.Tx, idxKey []byte) (*chainhash.Hash, int32, error) { 51 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 52 serialized := indexesBucket.Get(idxKey) 53 if len(serialized) < chainhash.HashSize+4 { 54 return nil, 0, database.Error{ 55 ErrorCode: database.ErrCorruption, 56 Description: fmt.Sprintf("unexpected end of data for "+ 57 "index %q tip", string(idxKey)), 58 } 59 } 60 61 var hash chainhash.Hash 62 copy(hash[:], serialized[:chainhash.HashSize]) 63 height := int32(byteOrder.Uint32(serialized[chainhash.HashSize:])) 64 return &hash, height, nil 65 } 66 67 // dbIndexConnectBlock adds all of the index entries associated with the 68 // given block using the provided indexer and updates the tip of the indexer 69 // accordingly. An error will be returned if the current tip for the indexer is 70 // not the previous block for the passed block. 71 func dbIndexConnectBlock(dbTx database.Tx, indexer Indexer, block *btcutil.Block, 72 stxo []blockchain.SpentTxOut) error { 73 74 // Assert that the block being connected properly connects to the 75 // current tip of the index. 76 idxKey := indexer.Key() 77 curTipHash, _, err := dbFetchIndexerTip(dbTx, idxKey) 78 if err != nil { 79 return err 80 } 81 if !curTipHash.IsEqual(&block.MsgBlock().Header.PrevBlock) { 82 return AssertError(fmt.Sprintf("dbIndexConnectBlock must be "+ 83 "called with a block that extends the current index "+ 84 "tip (%s, tip %s, block %s)", indexer.Name(), 85 curTipHash, block.Hash())) 86 } 87 88 // Notify the indexer with the connected block so it can index it. 89 if err := indexer.ConnectBlock(dbTx, block, stxo); err != nil { 90 return err 91 } 92 93 // Update the current index tip. 94 return dbPutIndexerTip(dbTx, idxKey, block.Hash(), block.Height()) 95 } 96 97 // dbIndexDisconnectBlock removes all of the index entries associated with the 98 // given block using the provided indexer and updates the tip of the indexer 99 // accordingly. An error will be returned if the current tip for the indexer is 100 // not the passed block. 101 func dbIndexDisconnectBlock(dbTx database.Tx, indexer Indexer, block *btcutil.Block, 102 stxo []blockchain.SpentTxOut) error { 103 104 // Assert that the block being disconnected is the current tip of the 105 // index. 106 idxKey := indexer.Key() 107 curTipHash, _, err := dbFetchIndexerTip(dbTx, idxKey) 108 if err != nil { 109 return err 110 } 111 if !curTipHash.IsEqual(block.Hash()) { 112 return AssertError(fmt.Sprintf("dbIndexDisconnectBlock must "+ 113 "be called with the block at the current index tip "+ 114 "(%s, tip %s, block %s)", indexer.Name(), 115 curTipHash, block.Hash())) 116 } 117 118 // Notify the indexer with the disconnected block so it can remove all 119 // of the appropriate entries. 120 if err := indexer.DisconnectBlock(dbTx, block, stxo); err != nil { 121 return err 122 } 123 124 // Update the current index tip. 125 prevHash := &block.MsgBlock().Header.PrevBlock 126 return dbPutIndexerTip(dbTx, idxKey, prevHash, block.Height()-1) 127 } 128 129 // Manager defines an index manager that manages multiple optional indexes and 130 // implements the blockchain.IndexManager interface so it can be seamlessly 131 // plugged into normal chain processing. 132 type Manager struct { 133 db database.DB 134 enabledIndexes []Indexer 135 } 136 137 // Ensure the Manager type implements the blockchain.IndexManager interface. 138 var _ blockchain.IndexManager = (*Manager)(nil) 139 140 // indexDropKey returns the key for an index which indicates it is in the 141 // process of being dropped. 142 func indexDropKey(idxKey []byte) []byte { 143 dropKey := make([]byte, len(idxKey)+1) 144 dropKey[0] = 'd' 145 copy(dropKey[1:], idxKey) 146 return dropKey 147 } 148 149 // maybeFinishDrops determines if each of the enabled indexes are in the middle 150 // of being dropped and finishes dropping them when the are. This is necessary 151 // because dropping and index has to be done in several atomic steps rather than 152 // one big atomic step due to the massive number of entries. 153 func (m *Manager) maybeFinishDrops(interrupt <-chan struct{}) error { 154 indexNeedsDrop := make([]bool, len(m.enabledIndexes)) 155 err := m.db.View(func(dbTx database.Tx) error { 156 // None of the indexes needs to be dropped if the index tips 157 // bucket hasn't been created yet. 158 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 159 if indexesBucket == nil { 160 return nil 161 } 162 163 // Mark the indexer as requiring a drop if one is already in 164 // progress. 165 for i, indexer := range m.enabledIndexes { 166 dropKey := indexDropKey(indexer.Key()) 167 if indexesBucket.Get(dropKey) != nil { 168 indexNeedsDrop[i] = true 169 } 170 } 171 172 return nil 173 }) 174 if err != nil { 175 return err 176 } 177 178 if interruptRequested(interrupt) { 179 return errInterruptRequested 180 } 181 182 // Finish dropping any of the enabled indexes that are already in the 183 // middle of being dropped. 184 for i, indexer := range m.enabledIndexes { 185 if !indexNeedsDrop[i] { 186 continue 187 } 188 189 log.Infof("Resuming %s drop", indexer.Name()) 190 err := dropIndex(m.db, indexer.Key(), indexer.Name(), interrupt) 191 if err != nil { 192 return err 193 } 194 } 195 196 return nil 197 } 198 199 // maybeCreateIndexes determines if each of the enabled indexes have already 200 // been created and creates them if not. 201 func (m *Manager) maybeCreateIndexes(dbTx database.Tx) error { 202 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 203 for _, indexer := range m.enabledIndexes { 204 // Nothing to do if the index tip already exists. 205 idxKey := indexer.Key() 206 if indexesBucket.Get(idxKey) != nil { 207 continue 208 } 209 210 // The tip for the index does not exist, so create it and 211 // invoke the create callback for the index so it can perform 212 // any one-time initialization it requires. 213 if err := indexer.Create(dbTx); err != nil { 214 return err 215 } 216 217 // Set the tip for the index to values which represent an 218 // uninitialized index. 219 err := dbPutIndexerTip(dbTx, idxKey, &chainhash.Hash{}, -1) 220 if err != nil { 221 return err 222 } 223 } 224 225 return nil 226 } 227 228 // Init initializes the enabled indexes. This is called during chain 229 // initialization and primarily consists of catching up all indexes to the 230 // current best chain tip. This is necessary since each index can be disabled 231 // and re-enabled at any time and attempting to catch-up indexes at the same 232 // time new blocks are being downloaded would lead to an overall longer time to 233 // catch up due to the I/O contention. 234 // 235 // This is part of the blockchain.IndexManager interface. 236 func (m *Manager) Init(chain *blockchain.BlockChain, interrupt <-chan struct{}) error { 237 // Nothing to do when no indexes are enabled. 238 if len(m.enabledIndexes) == 0 { 239 return nil 240 } 241 242 if interruptRequested(interrupt) { 243 return errInterruptRequested 244 } 245 246 // Finish and drops that were previously interrupted. 247 if err := m.maybeFinishDrops(interrupt); err != nil { 248 return err 249 } 250 251 // Create the initial state for the indexes as needed. 252 err := m.db.Update(func(dbTx database.Tx) error { 253 // Create the bucket for the current tips as needed. 254 meta := dbTx.Metadata() 255 _, err := meta.CreateBucketIfNotExists(indexTipsBucketName) 256 if err != nil { 257 return err 258 } 259 260 return m.maybeCreateIndexes(dbTx) 261 }) 262 if err != nil { 263 return err 264 } 265 266 // Initialize each of the enabled indexes. 267 for _, indexer := range m.enabledIndexes { 268 if err := indexer.Init(); err != nil { 269 return err 270 } 271 } 272 273 // Rollback indexes to the main chain if their tip is an orphaned fork. 274 // This is fairly unlikely, but it can happen if the chain is 275 // reorganized while the index is disabled. This has to be done in 276 // reverse order because later indexes can depend on earlier ones. 277 for i := len(m.enabledIndexes); i > 0; i-- { 278 indexer := m.enabledIndexes[i-1] 279 280 // Fetch the current tip for the index. 281 var height int32 282 var hash *chainhash.Hash 283 err := m.db.View(func(dbTx database.Tx) error { 284 idxKey := indexer.Key() 285 hash, height, err = dbFetchIndexerTip(dbTx, idxKey) 286 return err 287 }) 288 if err != nil { 289 return err 290 } 291 292 // Nothing to do if the index does not have any entries yet. 293 if height == -1 { 294 continue 295 } 296 297 // Loop until the tip is a block that exists in the main chain. 298 initialHeight := height 299 for !chain.MainChainHasBlock(hash) { 300 // At this point the index tip is orphaned, so load the 301 // orphaned block from the database directly and 302 // disconnect it from the index. The block has to be 303 // loaded directly since it is no longer in the main 304 // chain and thus the chain.BlockByHash function would 305 // error. 306 var block *btcutil.Block 307 err := m.db.View(func(dbTx database.Tx) error { 308 blockBytes, err := dbTx.FetchBlock(hash) 309 if err != nil { 310 return err 311 } 312 block, err = btcutil.NewBlockFromBytes(blockBytes) 313 if err != nil { 314 return err 315 } 316 block.SetHeight(height) 317 return err 318 }) 319 if err != nil { 320 return err 321 } 322 323 // We'll also grab the set of outputs spent by this 324 // block so we can remove them from the index. 325 spentTxos, err := chain.FetchSpendJournal(block) 326 if err != nil { 327 return err 328 } 329 330 // With the block and stxo set for that block retrieved, 331 // we can now update the index itself. 332 err = m.db.Update(func(dbTx database.Tx) error { 333 // Remove all of the index entries associated 334 // with the block and update the indexer tip. 335 err = dbIndexDisconnectBlock( 336 dbTx, indexer, block, spentTxos, 337 ) 338 if err != nil { 339 return err 340 } 341 342 // Update the tip to the previous block. 343 hash = &block.MsgBlock().Header.PrevBlock 344 height-- 345 346 return nil 347 }) 348 if err != nil { 349 return err 350 } 351 352 if interruptRequested(interrupt) { 353 return errInterruptRequested 354 } 355 } 356 357 if initialHeight != height { 358 log.Infof("Removed %d orphaned blocks from %s "+ 359 "(heights %d to %d)", initialHeight-height, 360 indexer.Name(), height+1, initialHeight) 361 } 362 } 363 364 // Fetch the current tip heights for each index along with tracking the 365 // lowest one so the catchup code only needs to start at the earliest 366 // block and is able to skip connecting the block for the indexes that 367 // don't need it. 368 bestHeight := chain.BestSnapshot().Height 369 lowestHeight := bestHeight 370 indexerHeights := make([]int32, len(m.enabledIndexes)) 371 err = m.db.View(func(dbTx database.Tx) error { 372 for i, indexer := range m.enabledIndexes { 373 idxKey := indexer.Key() 374 hash, height, err := dbFetchIndexerTip(dbTx, idxKey) 375 if err != nil { 376 return err 377 } 378 379 log.Debugf("Current %s tip (height %d, hash %v)", 380 indexer.Name(), height, hash) 381 indexerHeights[i] = height 382 if height < lowestHeight { 383 lowestHeight = height 384 } 385 } 386 return nil 387 }) 388 if err != nil { 389 return err 390 } 391 392 // Nothing to index if all of the indexes are caught up. 393 if lowestHeight == bestHeight { 394 return nil 395 } 396 397 // Create a progress logger for the indexing process below. 398 progressLogger := newBlockProgressLogger("Indexed", log) 399 400 // At this point, one or more indexes are behind the current best chain 401 // tip and need to be caught up, so log the details and loop through 402 // each block that needs to be indexed. 403 log.Infof("Catching up indexes from height %d to %d", lowestHeight, 404 bestHeight) 405 for height := lowestHeight + 1; height <= bestHeight; height++ { 406 // Load the block for the height since it is required to index 407 // it. 408 block, err := chain.BlockByHeight(height) 409 if err != nil { 410 return err 411 } 412 413 if interruptRequested(interrupt) { 414 return errInterruptRequested 415 } 416 417 // Connect the block for all indexes that need it. 418 var spentTxos []blockchain.SpentTxOut 419 for i, indexer := range m.enabledIndexes { 420 // Skip indexes that don't need to be updated with this 421 // block. 422 if indexerHeights[i] >= height { 423 continue 424 } 425 426 // When the index requires all of the referenced txouts 427 // and they haven't been loaded yet, they need to be 428 // retrieved from the spend journal. 429 if spentTxos == nil && indexNeedsInputs(indexer) { 430 spentTxos, err = chain.FetchSpendJournal(block) 431 if err != nil { 432 return err 433 } 434 } 435 436 err := m.db.Update(func(dbTx database.Tx) error { 437 return dbIndexConnectBlock( 438 dbTx, indexer, block, spentTxos, 439 ) 440 }) 441 if err != nil { 442 return err 443 } 444 indexerHeights[i] = height 445 } 446 447 // Log indexing progress. 448 progressLogger.LogBlockHeight(block) 449 450 if interruptRequested(interrupt) { 451 return errInterruptRequested 452 } 453 } 454 455 log.Infof("Indexes caught up to height %d", bestHeight) 456 return nil 457 } 458 459 // indexNeedsInputs returns whether or not the index needs access to the txouts 460 // referenced by the transaction inputs being indexed. 461 func indexNeedsInputs(index Indexer) bool { 462 if idx, ok := index.(NeedsInputser); ok { 463 return idx.NeedsInputs() 464 } 465 466 return false 467 } 468 469 // dbFetchTx looks up the passed transaction hash in the transaction index and 470 // loads it from the database. 471 func dbFetchTx(dbTx database.Tx, hash *chainhash.Hash) (*wire.MsgTx, error) { 472 // Look up the location of the transaction. 473 blockRegion, err := dbFetchTxIndexEntry(dbTx, hash) 474 if err != nil { 475 return nil, err 476 } 477 if blockRegion == nil { 478 return nil, fmt.Errorf("transaction %v not found", hash) 479 } 480 481 // Load the raw transaction bytes from the database. 482 txBytes, err := dbTx.FetchBlockRegion(blockRegion) 483 if err != nil { 484 return nil, err 485 } 486 487 // Deserialize the transaction. 488 var msgTx wire.MsgTx 489 err = msgTx.Deserialize(bytes.NewReader(txBytes)) 490 if err != nil { 491 return nil, err 492 } 493 494 return &msgTx, nil 495 } 496 497 // ConnectBlock must be invoked when a block is extending the main chain. It 498 // keeps track of the state of each index it is managing, performs some sanity 499 // checks, and invokes each indexer. 500 // 501 // This is part of the blockchain.IndexManager interface. 502 func (m *Manager) ConnectBlock(dbTx database.Tx, block *btcutil.Block, 503 stxos []blockchain.SpentTxOut) error { 504 505 // Call each of the currently active optional indexes with the block 506 // being connected so they can update accordingly. 507 for _, index := range m.enabledIndexes { 508 err := dbIndexConnectBlock(dbTx, index, block, stxos) 509 if err != nil { 510 return err 511 } 512 } 513 return nil 514 } 515 516 // DisconnectBlock must be invoked when a block is being disconnected from the 517 // end of the main chain. It keeps track of the state of each index it is 518 // managing, performs some sanity checks, and invokes each indexer to remove 519 // the index entries associated with the block. 520 // 521 // This is part of the blockchain.IndexManager interface. 522 func (m *Manager) DisconnectBlock(dbTx database.Tx, block *btcutil.Block, 523 stxo []blockchain.SpentTxOut) error { 524 525 // Call each of the currently active optional indexes with the block 526 // being disconnected so they can update accordingly. 527 for _, index := range m.enabledIndexes { 528 err := dbIndexDisconnectBlock(dbTx, index, block, stxo) 529 if err != nil { 530 return err 531 } 532 } 533 return nil 534 } 535 536 // NewManager returns a new index manager with the provided indexes enabled. 537 // 538 // The manager returned satisfies the blockchain.IndexManager interface and thus 539 // cleanly plugs into the normal blockchain processing path. 540 func NewManager(db database.DB, enabledIndexes []Indexer) *Manager { 541 return &Manager{ 542 db: db, 543 enabledIndexes: enabledIndexes, 544 } 545 } 546 547 // dropIndex drops the passed index from the database. Since indexes can be 548 // massive, it deletes the index in multiple database transactions in order to 549 // keep memory usage to reasonable levels. It also marks the drop in progress 550 // so the drop can be resumed if it is stopped before it is done before the 551 // index can be used again. 552 func dropIndex(db database.DB, idxKey []byte, idxName string, interrupt <-chan struct{}) error { 553 // Nothing to do if the index doesn't already exist. 554 var needsDelete bool 555 err := db.View(func(dbTx database.Tx) error { 556 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 557 if indexesBucket != nil && indexesBucket.Get(idxKey) != nil { 558 needsDelete = true 559 } 560 return nil 561 }) 562 if err != nil { 563 return err 564 } 565 if !needsDelete { 566 log.Infof("Not dropping %s because it does not exist", idxName) 567 return nil 568 } 569 570 // Mark that the index is in the process of being dropped so that it 571 // can be resumed on the next start if interrupted before the process is 572 // complete. 573 log.Infof("Dropping all %s entries. This might take a while...", 574 idxName) 575 err = db.Update(func(dbTx database.Tx) error { 576 indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) 577 return indexesBucket.Put(indexDropKey(idxKey), idxKey) 578 }) 579 if err != nil { 580 return err 581 } 582 583 // Since the indexes can be so large, attempting to simply delete 584 // the bucket in a single database transaction would result in massive 585 // memory usage and likely crash many systems due to ulimits. In order 586 // to avoid this, use a cursor to delete a maximum number of entries out 587 // of the bucket at a time. Recurse buckets depth-first to delete any 588 // sub-buckets. 589 const maxDeletions = 2000000 590 var totalDeleted uint64 591 592 // Recurse through all buckets in the index, cataloging each for 593 // later deletion. 594 var subBuckets [][][]byte 595 var subBucketClosure func(database.Tx, []byte, [][]byte) error 596 subBucketClosure = func(dbTx database.Tx, 597 subBucket []byte, tlBucket [][]byte) error { 598 // Get full bucket name and append to subBuckets for later 599 // deletion. 600 var bucketName [][]byte 601 if (tlBucket == nil) || (len(tlBucket) == 0) { 602 bucketName = append(bucketName, subBucket) 603 } else { 604 bucketName = append(tlBucket, subBucket) 605 } 606 subBuckets = append(subBuckets, bucketName) 607 // Recurse sub-buckets to append to subBuckets slice. 608 bucket := dbTx.Metadata() 609 for _, subBucketName := range bucketName { 610 bucket = bucket.Bucket(subBucketName) 611 } 612 return bucket.ForEachBucket(func(k []byte) error { 613 return subBucketClosure(dbTx, k, bucketName) 614 }) 615 } 616 617 // Call subBucketClosure with top-level bucket. 618 err = db.View(func(dbTx database.Tx) error { 619 return subBucketClosure(dbTx, idxKey, nil) 620 }) 621 if err != nil { 622 return nil 623 } 624 625 // Iterate through each sub-bucket in reverse, deepest-first, deleting 626 // all keys inside them and then dropping the buckets themselves. 627 for i := range subBuckets { 628 bucketName := subBuckets[len(subBuckets)-1-i] 629 // Delete maxDeletions key/value pairs at a time. 630 for numDeleted := maxDeletions; numDeleted == maxDeletions; { 631 numDeleted = 0 632 err := db.Update(func(dbTx database.Tx) error { 633 subBucket := dbTx.Metadata() 634 for _, subBucketName := range bucketName { 635 subBucket = subBucket.Bucket(subBucketName) 636 } 637 cursor := subBucket.Cursor() 638 for ok := cursor.First(); ok; ok = cursor.Next() && 639 numDeleted < maxDeletions { 640 641 if err := cursor.Delete(); err != nil { 642 return err 643 } 644 numDeleted++ 645 } 646 return nil 647 }) 648 if err != nil { 649 return err 650 } 651 652 if numDeleted > 0 { 653 totalDeleted += uint64(numDeleted) 654 log.Infof("Deleted %d keys (%d total) from %s", 655 numDeleted, totalDeleted, idxName) 656 } 657 } 658 659 if interruptRequested(interrupt) { 660 return errInterruptRequested 661 } 662 663 // Drop the bucket itself. 664 err = db.Update(func(dbTx database.Tx) error { 665 bucket := dbTx.Metadata() 666 for j := 0; j < len(bucketName)-1; j++ { 667 bucket = bucket.Bucket(bucketName[j]) 668 } 669 return bucket.DeleteBucket(bucketName[len(bucketName)-1]) 670 }) 671 } 672 673 // Call extra index specific deinitialization for the transaction index. 674 if idxName == txIndexName { 675 if err := dropBlockIDIndex(db); err != nil { 676 return err 677 } 678 } 679 680 // Remove the index tip, index bucket, and in-progress drop flag now 681 // that all index entries have been removed. 682 err = db.Update(func(dbTx database.Tx) error { 683 meta := dbTx.Metadata() 684 indexesBucket := meta.Bucket(indexTipsBucketName) 685 if err := indexesBucket.Delete(idxKey); err != nil { 686 return err 687 } 688 689 return indexesBucket.Delete(indexDropKey(idxKey)) 690 }) 691 if err != nil { 692 return err 693 } 694 695 log.Infof("Dropped %s", idxName) 696 return nil 697 }