github.com/deso-protocol/core@v1.2.9/lib/txindex.go (about)

     1  package lib
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"github.com/dgraph-io/badger/v3"
     7  	"path/filepath"
     8  	"reflect"
     9  	"sync"
    10  	"time"
    11  
    12  	chainlib "github.com/btcsuite/btcd/blockchain"
    13  	"github.com/deso-protocol/go-deadlock"
    14  	"github.com/golang/glog"
    15  )
    16  
    17  type TXIndex struct {
    18  	// TXIndexLock protects the transaction index.
    19  	TXIndexLock deadlock.RWMutex
    20  
    21  	// The txindex has it s own separate Blockchain object. This allows us to
    22  	// capture more metadata when collecting transactions without interfering
    23  	// with the goings-on of the main chain.
    24  	TXIndexChain *Blockchain
    25  
    26  	// Core objects from Server
    27  	CoreChain *Blockchain
    28  
    29  	// Core params object
    30  	Params *DeSoParams
    31  
    32  	// Update wait group
    33  	updateWaitGroup sync.WaitGroup
    34  
    35  	// Shutdown channel
    36  	stopUpdateChannel chan struct{}
    37  }
    38  
    39  func NewTXIndex(coreChain *Blockchain, params *DeSoParams, dataDirectory string) (*TXIndex, error) {
    40  	// Initialize database
    41  	txIndexDir := filepath.Join(GetBadgerDbPath(dataDirectory), "txindex")
    42  	txIndexOpts := badger.DefaultOptions(txIndexDir)
    43  	txIndexOpts.ValueDir = GetBadgerDbPath(txIndexDir)
    44  	txIndexOpts.MemTableSize = 1024 << 20
    45  	glog.Infof("TxIndex BadgerDB Dir: %v", txIndexOpts.Dir)
    46  	glog.Infof("TxIndex BadgerDB ValueDir: %v", txIndexOpts.ValueDir)
    47  	txIndexDb, err := badger.Open(txIndexOpts)
    48  	if err != nil {
    49  		glog.Fatal(err)
    50  	}
    51  
    52  	// See if we have a best chain hash stored in the txindex db.
    53  	bestBlockHashBeforeInit := DbGetBestHash(txIndexDb, ChainTypeDeSoBlock)
    54  
    55  	// If we haven't initialized the txIndexChain before, set up the
    56  	// seed mappings.
    57  	if bestBlockHashBeforeInit == nil {
    58  
    59  		// Add the seed balances. Originate them from the architect public key and
    60  		// set their block as the genesis block.
    61  		{
    62  			dummyPk := ArchitectPubKeyBase58Check
    63  			dummyTxn := &MsgDeSoTxn{
    64  				TxInputs:  []*DeSoInput{},
    65  				TxOutputs: params.SeedBalances,
    66  				TxnMeta:   &BlockRewardMetadataa{},
    67  				PublicKey: MustBase58CheckDecode(dummyPk),
    68  			}
    69  			affectedPublicKeys := []*AffectedPublicKey{}
    70  			totalOutput := uint64(0)
    71  			for _, seedBal := range params.SeedBalances {
    72  				affectedPublicKeys = append(affectedPublicKeys, &AffectedPublicKey{
    73  					PublicKeyBase58Check: PkToString(seedBal.PublicKey, params),
    74  					Metadata:             "GenesisBlockSeedBalance",
    75  				})
    76  				totalOutput += seedBal.AmountNanos
    77  			}
    78  			err := DbPutTxindexTransactionMappings(txIndexDb, dummyTxn, params, &TransactionMetadata{
    79  				TransactorPublicKeyBase58Check: dummyPk,
    80  				AffectedPublicKeys:             affectedPublicKeys,
    81  				BlockHashHex:                   GenesisBlockHashHex,
    82  				TxnIndexInBlock:                uint64(0),
    83  				// Just set some dummy metadata
    84  				BasicTransferTxindexMetadata: &BasicTransferTxindexMetadata{
    85  					TotalInputNanos:  0,
    86  					TotalOutputNanos: totalOutput,
    87  					FeeNanos:         0,
    88  				},
    89  			})
    90  			if err != nil {
    91  				return nil, fmt.Errorf("NewTXIndex: Error initializing seed balances in txindex: %v", err)
    92  			}
    93  		}
    94  
    95  		// Add the other seed txns to the txn index.
    96  		for txnIndex, txnHex := range params.SeedTxns {
    97  			txnBytes, err := hex.DecodeString(txnHex)
    98  			if err != nil {
    99  				return nil, fmt.Errorf("NewTXIndex: Error decoding seed txn HEX: %v, txn index: %v, txn hex: %v", err, txnIndex, txnHex)
   100  			}
   101  			txn := &MsgDeSoTxn{}
   102  			if err := txn.FromBytes(txnBytes); err != nil {
   103  				return nil, fmt.Errorf("NewTXIndex: Error decoding seed txn BYTES: %v, txn index: %v, txn hex: %v", err, txnIndex, txnHex)
   104  			}
   105  			err = DbPutTxindexTransactionMappings(txIndexDb, txn, params, &TransactionMetadata{
   106  				TransactorPublicKeyBase58Check: PkToString(txn.PublicKey, params),
   107  				// Note that we don't set AffectedPublicKeys for the SeedTxns
   108  				BlockHashHex:    GenesisBlockHashHex,
   109  				TxnIndexInBlock: uint64(0),
   110  				// Just set some dummy metadata
   111  				BasicTransferTxindexMetadata: &BasicTransferTxindexMetadata{
   112  					TotalInputNanos:  0,
   113  					TotalOutputNanos: 0,
   114  					FeeNanos:         0,
   115  				},
   116  			})
   117  			if err != nil {
   118  				return nil, fmt.Errorf("NewTXIndex: Error initializing seed txn %v in txindex: %v", txn, err)
   119  			}
   120  		}
   121  	}
   122  
   123  	// Ignore all the notifications from the txindex blockchain object
   124  	txIndexBlockchainNotificationChan := make(chan *ServerMessage, 1000)
   125  	go func() {
   126  		for {
   127  			<-txIndexBlockchainNotificationChan
   128  		}
   129  	}()
   130  
   131  	// Note that we *DONT* pass server here because it is already tied to the to the main blockchain.
   132  	txIndexChain, err := NewBlockchain(
   133  		[]string{}, 0,
   134  		params, chainlib.NewMedianTime(), txIndexDb, nil, nil)
   135  	if err != nil {
   136  		return nil, fmt.Errorf("NewTXIndex: Error initializing TxIndex: %v", err)
   137  	}
   138  
   139  	// At this point, we should have set up a blockchain object for our
   140  	// txindex, and initialized all of the seed txns and seed balances
   141  	// correctly. Attaching blocks to our txnindex blockchain or adding
   142  	// txns to our txindex should work smoothly now.
   143  
   144  	return &TXIndex{
   145  		TXIndexChain:      txIndexChain,
   146  		CoreChain:         coreChain,
   147  		Params:            params,
   148  		stopUpdateChannel: make(chan struct{}),
   149  	}, nil
   150  }
   151  
   152  func (txi *TXIndex) Start() {
   153  	glog.Info("TXIndex: Starting update thread")
   154  
   155  	// Run a loop to continuously update the txindex. Note that this is a noop
   156  	// except when run the first time or when a new block has arrived.
   157  	go func() {
   158  		txi.updateWaitGroup.Add(1)
   159  
   160  		for {
   161  			select {
   162  			case <-txi.stopUpdateChannel:
   163  				txi.updateWaitGroup.Done()
   164  				return
   165  			default:
   166  				if txi.CoreChain.ChainState() == SyncStateFullyCurrent {
   167  					// If the node is fully synced, then try an update.
   168  					err := txi.Update()
   169  					if err != nil {
   170  						glog.Error(fmt.Errorf("tryUpdateTxindex: Problem running update: %v", err))
   171  					}
   172  				} else {
   173  					glog.V(1).Infof("TXIndex: Waiting for node to sync before updating")
   174  				}
   175  				break
   176  			}
   177  
   178  			time.Sleep(1 * time.Second)
   179  		}
   180  	}()
   181  }
   182  
   183  func (txi *TXIndex) Stop() {
   184  	glog.Info("TXIndex: Stopping updates and closing database")
   185  
   186  	txi.stopUpdateChannel <- struct{}{}
   187  	txi.updateWaitGroup.Wait()
   188  
   189  	txi.TXIndexChain.DB().Close()
   190  }
   191  
   192  // GetTxindexUpdateBlockNodes ...
   193  func (txi *TXIndex) GetTxindexUpdateBlockNodes() (
   194  	_txindexTipNode *BlockNode, _blockTipNode *BlockNode, _commonAncestor *BlockNode,
   195  	_detachBlocks []*BlockNode, _attachBlocks []*BlockNode) {
   196  
   197  	// Get the current txindex tip.
   198  	txindexTipHash := txi.TXIndexChain.BlockTip()
   199  	if txindexTipHash == nil {
   200  		// The tip hash should never be nil since the txindex chain should have
   201  		// been initialized in the constructor. Print an error and return in this
   202  		// case.
   203  		glog.Error("Error: TXIndexChain had nil tip; this should never " +
   204  			"happen and it means the transaction index is broken.")
   205  		return nil, nil, nil, nil, nil
   206  	}
   207  	// If the tip of the txindex is no longer stored in the block index, it
   208  	// means the txindex hit a fork that we are no longer keeping track of.
   209  	// The only thing we can really do in this case is rebuild the entire index
   210  	// from scratch. To do that, we return all the blocks in the index to detach
   211  	// and all the blocks in the real chain to attach.
   212  	txindexTipNode := txi.CoreChain.CopyBlockIndex()[*txindexTipHash.Hash]
   213  
   214  	if txindexTipNode == nil {
   215  		glog.Info("GetTxindexUpdateBlockNodes: Txindex tip was not found; building txindex starting at genesis block")
   216  
   217  		newTxIndexBestChain, _ := txi.TXIndexChain.CopyBestChain()
   218  		newBlockchainBestChain, _ := txi.CoreChain.CopyBestChain()
   219  
   220  		return txindexTipNode, txi.CoreChain.BlockTip(), nil, newTxIndexBestChain, newBlockchainBestChain
   221  	}
   222  
   223  	// At this point, we know our txindex tip is in our block index so
   224  	// there must be a common ancestor between the tip and the block tip.
   225  	blockTip := txi.CoreChain.BlockTip()
   226  	commonAncestor, detachBlocks, attachBlocks := GetReorgBlocks(txindexTipNode, blockTip)
   227  
   228  	return txindexTipNode, blockTip, commonAncestor, detachBlocks, attachBlocks
   229  }
   230  
   231  // Update syncs the transaction index with the blockchain.
   232  // Specifically, it reads in all the blocks that have come in since the last
   233  // time this function was called and adds the new transactions to the txindex.
   234  // It also handles reorgs properly.
   235  //
   236  // TODO(DELETEME, cleanup): This code is error-prone. Moving the transaction indexing code
   237  // to block_view.go may be a clean way to refactor this.
   238  func (txi *TXIndex) Update() error {
   239  	// If we don't have a chain set, return an error.
   240  	if txi.TXIndexChain == nil {
   241  		return fmt.Errorf("Update: Missing TXIndexChain")
   242  	}
   243  
   244  	// Lock the txindex and the blockchain for reading until we're
   245  	// done with the rest of the function.
   246  	txi.TXIndexLock.Lock()
   247  	defer txi.TXIndexLock.Unlock()
   248  	txindexTipNode, blockTipNode, commonAncestor, detachBlocks, attachBlocks := txi.GetTxindexUpdateBlockNodes()
   249  
   250  	// Note that the blockchain's ChainLock does not need to be held at this
   251  	// point because we're just reading blocks from the db, which never get
   252  	// deleted and therefore don't need the lock in order to access.
   253  
   254  	// If we get to this point, the commonAncestor should never be nil.
   255  	if commonAncestor == nil {
   256  		return fmt.Errorf("Update: Expected common ancestor "+
   257  			"between txindex tip %v and block tip %v but found none; this "+
   258  			"should never happen", txindexTipNode, blockTipNode)
   259  	}
   260  	// If the tip of the txindex is the same as the block tip, don't do
   261  	// an update.
   262  	if reflect.DeepEqual(txindexTipNode.Hash[:], blockTipNode.Hash[:]) {
   263  		glog.V(1).Infof("Update: Skipping update since block tip equals "+
   264  			"txindex tip: Height: %d, Hash: %v", txindexTipNode.Height, txindexTipNode.Hash)
   265  		return nil
   266  	}
   267  
   268  	// When the txindex tip does not match the block tip then there's work
   269  	// to do. Log at the info level.
   270  	glog.Infof("Update: Updating txindex tip (height: %d, hash: %v) "+
   271  		"to block tip (height: %d, hash: %v) ...",
   272  		txindexTipNode.Height, txindexTipNode.Hash,
   273  		blockTipNode.Height, blockTipNode.Hash)
   274  
   275  	// For each of the blocks we're removing, delete the transactions from
   276  	// the transaction index.
   277  	for _, blockToDetach := range detachBlocks {
   278  		// Go through each txn in the block and delete its mappings from our
   279  		// txindex.
   280  		glog.V(1).Infof("Update: Detaching block (height: %d, hash: %v)",
   281  			blockToDetach.Height, blockToDetach.Hash)
   282  		blockMsg, err := GetBlock(blockToDetach.Hash, txi.TXIndexChain.DB())
   283  		if err != nil {
   284  			return fmt.Errorf("Update: Problem fetching detach block "+
   285  				"with hash %v: %v", blockToDetach.Hash, err)
   286  		}
   287  		// Iterate through each transaction in the block and delete all its
   288  		// mappings from the db. Note the txindex has its own db that is
   289  		// distinct and isolated from our core blockchain db.
   290  		for _, txn := range blockMsg.Txns {
   291  			if err := DbDeleteTxindexTransactionMappings(
   292  				txi.TXIndexChain.DB(), txn, txi.Params); err != nil {
   293  
   294  				return fmt.Errorf("Update: Problem deleting "+
   295  					"transaction mappings for transaction %v: %v", txn.Hash(), err)
   296  			}
   297  		}
   298  
   299  		// Now that all the transactions have been deleted from our txindex,
   300  		// it's safe to disconnect the block from our txindex chain.
   301  		utxoView, err := NewUtxoView(txi.TXIndexChain.DB(), txi.Params, nil)
   302  		if err != nil {
   303  			return fmt.Errorf(
   304  				"Update: Error initializing UtxoView: %v", err)
   305  		}
   306  		utxoOps, err := GetUtxoOperationsForBlock(
   307  			txi.TXIndexChain.DB(), blockToDetach.Hash)
   308  		if err != nil {
   309  			return fmt.Errorf(
   310  				"Update: Error getting UtxoOps for block %v: %v", blockToDetach, err)
   311  		}
   312  		// Compute the hashes for all the transactions.
   313  		txHashes, err := ComputeTransactionHashes(blockMsg.Txns)
   314  		if err != nil {
   315  			return fmt.Errorf(
   316  				"Update: Error computing tx hashes for block %v: %v",
   317  				blockToDetach, err)
   318  		}
   319  		if err := utxoView.DisconnectBlock(blockMsg, txHashes, utxoOps); err != nil {
   320  			return fmt.Errorf("Update: Error detaching block "+
   321  				"%v from UtxoView: %v", blockToDetach, err)
   322  		}
   323  		if err := utxoView.FlushToDb(); err != nil {
   324  			return fmt.Errorf("Update: Error flushing view to db for block "+
   325  				"%v: %v", blockToDetach, err)
   326  		}
   327  		// We have to flush a couple of extra things that the view doesn't flush...
   328  		if err := PutBestHash(utxoView.TipHash, txi.TXIndexChain.DB(), ChainTypeDeSoBlock); err != nil {
   329  			return fmt.Errorf("Update: Error putting best hash for block "+
   330  				"%v: %v", blockToDetach, err)
   331  		}
   332  		err = txi.TXIndexChain.DB().Update(func(txn *badger.Txn) error {
   333  			if err := DeleteUtxoOperationsForBlockWithTxn(txn, blockToDetach.Hash); err != nil {
   334  				return fmt.Errorf("Update: Error deleting UtxoOperations 1 for block %v, %v", blockToDetach.Hash, err)
   335  			}
   336  			if err := txn.Delete(BlockHashToBlockKey(blockToDetach.Hash)); err != nil {
   337  				return fmt.Errorf("Update: Error deleting UtxoOperations 2 for block %v %v", blockToDetach.Hash, err)
   338  			}
   339  			return nil
   340  		})
   341  		if err != nil {
   342  			return fmt.Errorf("Update: Error updating badgger: %v", err)
   343  		}
   344  		// Delete this block from the chain db so we don't get duplicate block errors.
   345  
   346  		// Remove this block from our bestChain data structures.
   347  		newBlockIndex := txi.TXIndexChain.CopyBlockIndex()
   348  		newBestChain, newBestChainMap := txi.TXIndexChain.CopyBestChain()
   349  		newBestChain = newBestChain[:len(newBestChain)-1]
   350  		delete(newBestChainMap, *(blockToDetach.Hash))
   351  		delete(newBlockIndex, *(blockToDetach.Hash))
   352  
   353  		txi.TXIndexChain.SetBestChainMap(newBestChain, newBestChainMap, newBlockIndex)
   354  
   355  		// At this point the entries for the block should have been removed
   356  		// from both our Txindex chain and our transaction index mappings.
   357  	}
   358  
   359  	// For each of the blocks we're adding, process them on our txindex chain
   360  	// and add their mappings to our txn index. Compute any metadata that might
   361  	// be useful.
   362  	for _, blockToAttach := range attachBlocks {
   363  		if blockToAttach.Height%100 == 0 {
   364  			glog.Infof("Update: Txindex progress: block %d / %d",
   365  				blockToAttach.Height, blockTipNode.Height)
   366  		}
   367  		glog.V(2).Infof("Update: Attaching block (height: %d, hash: %v)",
   368  			blockToAttach.Height, blockToAttach.Hash)
   369  
   370  		blockMsg, err := GetBlock(blockToAttach.Hash, txi.CoreChain.DB())
   371  		if err != nil {
   372  			return fmt.Errorf("Update: Problem fetching attach block "+
   373  				"with hash %v: %v", blockToAttach.Hash, err)
   374  		}
   375  
   376  		// We use a view to simulate adding transactions to our chain. This allows
   377  		// us to extract custom metadata fields that we can show in our block explorer.
   378  		//
   379  		// Only set a BitcoinManager if we have one. This makes some tests pass.
   380  		utxoView, err := NewUtxoView(txi.TXIndexChain.DB(), txi.Params, nil)
   381  		if err != nil {
   382  			return fmt.Errorf(
   383  				"Update: Error initializing UtxoView: %v", err)
   384  		}
   385  
   386  		// Do each block update in a single transaction so we're safe in case the node
   387  		// restarts.
   388  		txi.TXIndexChain.DB().Update(func(dbTxn *badger.Txn) error {
   389  
   390  			// Iterate through each transaction in the block and do the following:
   391  			// - Connect it to the view
   392  			// - Compute its mapping values, which may include custom metadata fields
   393  			// - add all its mappings to the db.
   394  			for txnIndexInBlock, txn := range blockMsg.Txns {
   395  				txnMeta, err := ConnectTxnAndComputeTransactionMetadata(
   396  					txn, utxoView, blockToAttach.Hash, blockToAttach.Height, uint64(txnIndexInBlock))
   397  				if err != nil {
   398  					return fmt.Errorf("Update: Problem connecting txn %v to txindex: %v",
   399  						txn, err)
   400  				}
   401  
   402  				err = DbPutTxindexTransactionMappingsWithTxn(dbTxn, txn, txi.Params, txnMeta)
   403  				if err != nil {
   404  					return fmt.Errorf("Update: Problem adding txn %v to txindex: %v",
   405  						txn, err)
   406  				}
   407  			}
   408  
   409  			return nil
   410  		})
   411  
   412  		// Now that we have added all the txns to our TxIndex db, attach the block
   413  		// to update our chain.
   414  		_, _, err = txi.TXIndexChain.ProcessBlock(blockMsg, false /*verifySignatures*/)
   415  		if err != nil {
   416  			return fmt.Errorf("Update: Problem attaching block %v: %v",
   417  				blockToAttach, err)
   418  		}
   419  	}
   420  
   421  	glog.Infof("Update: Txindex update complete. New tip: (height: %d, hash: %v)",
   422  		txi.TXIndexChain.BlockTip().Height, txi.TXIndexChain.BlockTip().Hash)
   423  
   424  	return nil
   425  }