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 }