github.com/MetalBlockchain/metalgo@v1.11.9/vms/components/index/index.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package index
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/binary"
     9  	"errors"
    10  	"fmt"
    11  
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	"go.uber.org/zap"
    14  
    15  	"github.com/MetalBlockchain/metalgo/database"
    16  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    17  	"github.com/MetalBlockchain/metalgo/ids"
    18  	"github.com/MetalBlockchain/metalgo/utils/logging"
    19  	"github.com/MetalBlockchain/metalgo/utils/set"
    20  	"github.com/MetalBlockchain/metalgo/utils/wrappers"
    21  	"github.com/MetalBlockchain/metalgo/vms/components/avax"
    22  )
    23  
    24  var (
    25  	ErrIndexingRequiredFromGenesis = errors.New("running would create incomplete index. Allow incomplete indices or re-sync from genesis with indexing enabled")
    26  	ErrCausesIncompleteIndex       = errors.New("running would create incomplete index. Allow incomplete indices or enable indexing")
    27  
    28  	idxKey         = []byte("idx")
    29  	idxCompleteKey = []byte("complete")
    30  
    31  	_ AddressTxsIndexer = (*indexer)(nil)
    32  	_ AddressTxsIndexer = (*noIndexer)(nil)
    33  )
    34  
    35  // AddressTxsIndexer maintains information about which transactions changed
    36  // the balances of which addresses. This includes both transactions that
    37  // increase and decrease an address's balance.
    38  // A transaction is said to change an address's balance if either is true:
    39  // 1) A UTXO that the transaction consumes was at least partially owned by the address.
    40  // 2) A UTXO that the transaction produces is at least partially owned by the address.
    41  type AddressTxsIndexer interface {
    42  	// Accept is called when [txID] is accepted.
    43  	// Persists data about [txID] and what balances it changed.
    44  	// [inputUTXOs] are the UTXOs [txID] consumes.
    45  	// [outputUTXOs] are the UTXOs [txID] creates.
    46  	// If the error is non-nil, do not persist [txID] to disk as accepted in the VM
    47  	Accept(
    48  		txID ids.ID,
    49  		inputUTXOs []*avax.UTXO,
    50  		outputUTXOs []*avax.UTXO,
    51  	) error
    52  
    53  	// Read returns the IDs of transactions that changed [address]'s balance of [assetID].
    54  	// The returned transactions are in order of increasing acceptance time.
    55  	// The length of the returned slice <= [pageSize].
    56  	// [cursor] is the offset to start reading from.
    57  	Read(address []byte, assetID ids.ID, cursor, pageSize uint64) ([]ids.ID, error)
    58  }
    59  
    60  type indexer struct {
    61  	log     logging.Logger
    62  	metrics metrics
    63  	db      database.Database
    64  }
    65  
    66  // NewIndexer returns a new AddressTxsIndexer.
    67  // The returned indexer ignores UTXOs that are not type secp256k1fx.TransferOutput.
    68  func NewIndexer(
    69  	db database.Database,
    70  	log logging.Logger,
    71  	metricsNamespace string,
    72  	metricsRegisterer prometheus.Registerer,
    73  	allowIncompleteIndices bool,
    74  ) (AddressTxsIndexer, error) {
    75  	i := &indexer{
    76  		db:  db,
    77  		log: log,
    78  	}
    79  	// initialize the indexer
    80  	if err := checkIndexStatus(i.db, true, allowIncompleteIndices); err != nil {
    81  		return nil, err
    82  	}
    83  	// initialize the metrics
    84  	if err := i.metrics.initialize(metricsNamespace, metricsRegisterer); err != nil {
    85  		return nil, err
    86  	}
    87  	return i, nil
    88  }
    89  
    90  // Accept persists which balances [txID] changed.
    91  // Associates all UTXOs in [i.balanceChanges] with transaction [txID].
    92  // The database structure is:
    93  // [address]
    94  // |  [assetID]
    95  // |  |
    96  // |  | "idx" => 2 		Running transaction index key, represents the next index
    97  // |  | "0"   => txID1
    98  // |  | "1"   => txID1
    99  // See interface documentation AddressTxsIndexer.Accept
   100  func (i *indexer) Accept(txID ids.ID, inputUTXOs []*avax.UTXO, outputUTXOs []*avax.UTXO) error {
   101  	utxos := inputUTXOs
   102  	// Fetch and add the output UTXOs
   103  	utxos = append(utxos, outputUTXOs...)
   104  
   105  	// convert UTXOs into balance changes
   106  	// Address -> AssetID --> exists if the address's balance
   107  	// of the asset is changed by processing tx [txID]
   108  	// we do this step separately to simplify the write process later
   109  	balanceChanges := map[string]set.Set[ids.ID]{}
   110  	for _, utxo := range utxos {
   111  		out, ok := utxo.Out.(avax.Addressable)
   112  		if !ok {
   113  			i.log.Verbo("skipping UTXO for indexing",
   114  				zap.Stringer("utxoID", utxo.InputID()),
   115  			)
   116  			continue
   117  		}
   118  
   119  		for _, addressBytes := range out.Addresses() {
   120  			address := string(addressBytes)
   121  
   122  			addressChanges, exists := balanceChanges[address]
   123  			if !exists {
   124  				addressChanges = set.Set[ids.ID]{}
   125  				balanceChanges[address] = addressChanges
   126  			}
   127  			addressChanges.Add(utxo.AssetID())
   128  		}
   129  	}
   130  
   131  	// Process the balance changes
   132  	for address, assetIDs := range balanceChanges {
   133  		addressPrefixDB := prefixdb.New([]byte(address), i.db)
   134  		for assetID := range assetIDs {
   135  			assetPrefixDB := prefixdb.New(assetID[:], addressPrefixDB)
   136  
   137  			var idx uint64
   138  			idxBytes, err := assetPrefixDB.Get(idxKey)
   139  			switch err {
   140  			case nil:
   141  				// index is found, parse stored [idxBytes]
   142  				idx = binary.BigEndian.Uint64(idxBytes)
   143  			case database.ErrNotFound:
   144  				// idx not found; this must be the first entry.
   145  				idxBytes = make([]byte, wrappers.LongLen)
   146  			default:
   147  				// Unexpected error
   148  				return fmt.Errorf("unexpected error when indexing txID %s: %w", txID, err)
   149  			}
   150  
   151  			// write the [txID] at the index
   152  			i.log.Verbo("writing indexed tx to DB",
   153  				zap.String("address", address),
   154  				zap.Stringer("assetID", assetID),
   155  				zap.Uint64("index", idx),
   156  				zap.Stringer("txID", txID),
   157  			)
   158  			if err := assetPrefixDB.Put(idxBytes, txID[:]); err != nil {
   159  				return fmt.Errorf("failed to write txID while indexing %s: %w", txID, err)
   160  			}
   161  
   162  			// increment and store the index for next use
   163  			idx++
   164  			binary.BigEndian.PutUint64(idxBytes, idx)
   165  
   166  			if err := assetPrefixDB.Put(idxKey, idxBytes); err != nil {
   167  				return fmt.Errorf("failed to write index txID while indexing %s: %w", txID, err)
   168  			}
   169  		}
   170  	}
   171  	i.metrics.numTxsIndexed.Inc()
   172  	return nil
   173  }
   174  
   175  // Read returns IDs of transactions that changed [address]'s balance of [assetID],
   176  // starting at [cursor], in order of transaction acceptance. e.g. if [cursor] == 1, does
   177  // not return the first transaction that changed the balance. (This is for pagination.)
   178  // Returns at most [pageSize] elements.
   179  // See AddressTxsIndexer
   180  func (i *indexer) Read(address []byte, assetID ids.ID, cursor, pageSize uint64) ([]ids.ID, error) {
   181  	// setup prefix DBs
   182  	addressTxDB := prefixdb.New(address, i.db)
   183  	assetPrefixDB := prefixdb.New(assetID[:], addressTxDB)
   184  
   185  	// get cursor in bytes
   186  	cursorBytes := make([]byte, wrappers.LongLen)
   187  	binary.BigEndian.PutUint64(cursorBytes, cursor)
   188  
   189  	// start reading from the cursor bytes, numeric keys maintain the order (see Accept)
   190  	iter := assetPrefixDB.NewIteratorWithStart(cursorBytes)
   191  	defer iter.Release()
   192  
   193  	var txIDs []ids.ID
   194  	for uint64(len(txIDs)) < pageSize && iter.Next() {
   195  		if bytes.Equal(idxKey, iter.Key()) {
   196  			// This key has the next index to use, not a tx ID
   197  			continue
   198  		}
   199  
   200  		// get the value and try to convert it to ID
   201  		txIDBytes := iter.Value()
   202  		txID, err := ids.ToID(txIDBytes)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  
   207  		txIDs = append(txIDs, txID)
   208  	}
   209  	return txIDs, nil
   210  }
   211  
   212  // checkIndexStatus checks the indexing status in the database, returning error if the state
   213  // with respect to provided parameters is invalid
   214  func checkIndexStatus(db database.KeyValueReaderWriter, enableIndexing, allowIncomplete bool) error {
   215  	// verify whether the index is complete.
   216  	idxComplete, err := database.GetBool(db, idxCompleteKey)
   217  	if err == database.ErrNotFound {
   218  		// We've not run before. Mark whether indexing is enabled this run.
   219  		return database.PutBool(db, idxCompleteKey, enableIndexing)
   220  	} else if err != nil {
   221  		return err
   222  	}
   223  
   224  	if idxComplete && enableIndexing {
   225  		// indexing has been enabled in the past and we're enabling it now
   226  		return nil
   227  	}
   228  
   229  	if !idxComplete && enableIndexing && !allowIncomplete {
   230  		// In a previous run, we did not index so it's incomplete.
   231  		// indexing was disabled before but now we want to index.
   232  		return ErrIndexingRequiredFromGenesis
   233  	} else if !idxComplete {
   234  		// either indexing is disabled, or incomplete indices are ok, so we don't care that index is incomplete
   235  		return nil
   236  	}
   237  
   238  	// the index is complete
   239  	if !enableIndexing && !allowIncomplete { // indexing is disabled this run
   240  		return ErrCausesIncompleteIndex
   241  	} else if !enableIndexing {
   242  		// running without indexing makes it incomplete
   243  		return database.PutBool(db, idxCompleteKey, false)
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  type noIndexer struct{}
   250  
   251  func NewNoIndexer(db database.Database, allowIncomplete bool) (AddressTxsIndexer, error) {
   252  	return &noIndexer{}, checkIndexStatus(db, false, allowIncomplete)
   253  }
   254  
   255  func (*noIndexer) Accept(ids.ID, []*avax.UTXO, []*avax.UTXO) error {
   256  	return nil
   257  }
   258  
   259  func (*noIndexer) Read([]byte, ids.ID, uint64, uint64) ([]ids.ID, error) {
   260  	return nil, nil
   261  }