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