github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/state/txindex/kv/kv.go (about) 1 package kv 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "fmt" 8 "math/big" 9 "strconv" 10 "strings" 11 12 "github.com/cosmos/gogoproto/proto" 13 14 dbm "github.com/badrootd/nibiru-db" 15 16 abci "github.com/badrootd/nibiru-cometbft/abci/types" 17 "github.com/badrootd/nibiru-cometbft/libs/pubsub/query" 18 "github.com/badrootd/nibiru-cometbft/state/indexer" 19 "github.com/badrootd/nibiru-cometbft/state/txindex" 20 "github.com/badrootd/nibiru-cometbft/types" 21 ) 22 23 const ( 24 tagKeySeparator = "/" 25 eventSeqSeparator = "$es$" 26 ) 27 28 var _ txindex.TxIndexer = (*TxIndex)(nil) 29 30 // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB). 31 type TxIndex struct { 32 store dbm.DB 33 // Number the events in the event list 34 eventSeq int64 35 } 36 37 // NewTxIndex creates new KV indexer. 38 func NewTxIndex(store dbm.DB) *TxIndex { 39 return &TxIndex{ 40 store: store, 41 } 42 } 43 44 // Get gets transaction from the TxIndex storage and returns it or nil if the 45 // transaction is not found. 46 func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) { 47 if len(hash) == 0 { 48 return nil, txindex.ErrorEmptyHash 49 } 50 51 rawBytes, err := txi.store.Get(hash) 52 if err != nil { 53 panic(err) 54 } 55 if rawBytes == nil { 56 return nil, nil 57 } 58 59 txResult := new(abci.TxResult) 60 err = proto.Unmarshal(rawBytes, txResult) 61 if err != nil { 62 return nil, fmt.Errorf("error reading TxResult: %v", err) 63 } 64 65 return txResult, nil 66 } 67 68 // AddBatch indexes a batch of transactions using the given list of events. Each 69 // key that indexed from the tx's events is a composite of the event type and 70 // the respective attribute's key delimited by a "." (eg. "account.number"). 71 // Any event with an empty type is not indexed. 72 func (txi *TxIndex) AddBatch(b *txindex.Batch) error { 73 storeBatch := txi.store.NewBatch() 74 defer storeBatch.Close() 75 76 for _, result := range b.Ops { 77 hash := types.Tx(result.Tx).Hash() 78 79 // index tx by events 80 err := txi.indexEvents(result, hash, storeBatch) 81 if err != nil { 82 return err 83 } 84 85 // index by height (always) 86 err = storeBatch.Set(keyForHeight(result), hash) 87 if err != nil { 88 return err 89 } 90 91 rawBytes, err := proto.Marshal(result) 92 if err != nil { 93 return err 94 } 95 // index by hash (always) 96 err = storeBatch.Set(hash, rawBytes) 97 if err != nil { 98 return err 99 } 100 } 101 102 return storeBatch.WriteSync() 103 } 104 105 // Index indexes a single transaction using the given list of events. Each key 106 // that indexed from the tx's events is a composite of the event type and the 107 // respective attribute's key delimited by a "." (eg. "account.number"). 108 // Any event with an empty type is not indexed. 109 // 110 // If a transaction is indexed with the same hash as a previous transaction, it will 111 // be overwritten unless the tx result was NOT OK and the prior result was OK i.e. 112 // more transactions that successfully executed overwrite transactions that failed 113 // or successful yet older transactions. 114 func (txi *TxIndex) Index(result *abci.TxResult) error { 115 b := txi.store.NewBatch() 116 defer b.Close() 117 118 hash := types.Tx(result.Tx).Hash() 119 120 if !result.Result.IsOK() { 121 oldResult, err := txi.Get(hash) 122 if err != nil { 123 return err 124 } 125 126 // if the new transaction failed and it's already indexed in an older block and was successful 127 // we skip it as we want users to get the older successful transaction when they query. 128 if oldResult != nil && oldResult.Result.Code == abci.CodeTypeOK { 129 return nil 130 } 131 } 132 133 // index tx by events 134 err := txi.indexEvents(result, hash, b) 135 if err != nil { 136 return err 137 } 138 139 // index by height (always) 140 err = b.Set(keyForHeight(result), hash) 141 if err != nil { 142 return err 143 } 144 145 rawBytes, err := proto.Marshal(result) 146 if err != nil { 147 return err 148 } 149 // index by hash (always) 150 err = b.Set(hash, rawBytes) 151 if err != nil { 152 return err 153 } 154 155 return b.WriteSync() 156 } 157 158 func (txi *TxIndex) indexEvents(result *abci.TxResult, hash []byte, store dbm.Batch) error { 159 for _, event := range result.Result.Events { 160 txi.eventSeq = txi.eventSeq + 1 161 // only index events with a non-empty type 162 if len(event.Type) == 0 { 163 continue 164 } 165 166 for _, attr := range event.Attributes { 167 if len(attr.Key) == 0 { 168 continue 169 } 170 171 // index if `index: true` is set 172 compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key) 173 // ensure event does not conflict with a reserved prefix key 174 if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey { 175 return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag) 176 } 177 if attr.GetIndex() { 178 err := store.Set(keyForEvent(compositeTag, attr.Value, result, txi.eventSeq), hash) 179 if err != nil { 180 return err 181 } 182 } 183 } 184 } 185 186 return nil 187 } 188 189 // Search performs a search using the given query. 190 // 191 // It breaks the query into conditions (like "tx.height > 5"). For each 192 // condition, it queries the DB index. One special use cases here: (1) if 193 // "tx.hash" is found, it returns tx result for it (2) for range queries it is 194 // better for the client to provide both lower and upper bounds, so we are not 195 // performing a full scan. Results from querying indexes are then intersected 196 // and returned to the caller, in no particular order. 197 // 198 // Search will exit early and return any result fetched so far, 199 // when a message is received on the context chan. 200 func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) { 201 select { 202 case <-ctx.Done(): 203 return make([]*abci.TxResult, 0), nil 204 205 default: 206 } 207 208 var hashesInitialized bool 209 filteredHashes := make(map[string][]byte) 210 211 // get a list of conditions (like "tx.height > 5") 212 conditions, err := q.Conditions() 213 if err != nil { 214 return nil, fmt.Errorf("error during parsing conditions from query: %w", err) 215 } 216 217 // if there is a hash condition, return the result immediately 218 hash, ok, err := lookForHash(conditions) 219 if err != nil { 220 return nil, fmt.Errorf("error during searching for a hash in the query: %w", err) 221 } else if ok { 222 res, err := txi.Get(hash) 223 switch { 224 case err != nil: 225 return []*abci.TxResult{}, fmt.Errorf("error while retrieving the result: %w", err) 226 case res == nil: 227 return []*abci.TxResult{}, nil 228 default: 229 return []*abci.TxResult{res}, nil 230 } 231 } 232 233 // conditions to skip because they're handled before "everything else" 234 skipIndexes := make([]int, 0) 235 var heightInfo HeightInfo 236 237 // If we are not matching events and tx.height = 3 occurs more than once, the later value will 238 // overwrite the first one. 239 conditions, heightInfo = dedupHeight(conditions) 240 241 if !heightInfo.onlyHeightEq { 242 skipIndexes = append(skipIndexes, heightInfo.heightEqIdx) 243 } 244 245 // extract ranges 246 // if both upper and lower bounds exist, it's better to get them in order not 247 // no iterate over kvs that are not within range. 248 ranges, rangeIndexes, heightRange := indexer.LookForRangesWithHeight(conditions) 249 heightInfo.heightRange = heightRange 250 if len(ranges) > 0 { 251 skipIndexes = append(skipIndexes, rangeIndexes...) 252 253 for _, qr := range ranges { 254 255 // If we have a query range over height and want to still look for 256 // specific event values we do not want to simply return all 257 // transactios in this height range. We remember the height range info 258 // and pass it on to match() to take into account when processing events. 259 if qr.Key == types.TxHeightKey && !heightInfo.onlyHeightRange { 260 continue 261 } 262 if !hashesInitialized { 263 filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, true, heightInfo) 264 hashesInitialized = true 265 266 // Ignore any remaining conditions if the first condition resulted 267 // in no matches (assuming implicit AND operand). 268 if len(filteredHashes) == 0 { 269 break 270 } 271 } else { 272 filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, false, heightInfo) 273 } 274 } 275 } 276 277 // if there is a height condition ("tx.height=3"), extract it 278 279 // for all other conditions 280 for i, c := range conditions { 281 if intInSlice(i, skipIndexes) { 282 continue 283 } 284 285 if !hashesInitialized { 286 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, true, heightInfo) 287 hashesInitialized = true 288 289 // Ignore any remaining conditions if the first condition resulted 290 // in no matches (assuming implicit AND operand). 291 if len(filteredHashes) == 0 { 292 break 293 } 294 } else { 295 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, false, heightInfo) 296 } 297 } 298 299 results := make([]*abci.TxResult, 0, len(filteredHashes)) 300 resultMap := make(map[string]struct{}) 301 RESULTS_LOOP: 302 for _, h := range filteredHashes { 303 304 res, err := txi.Get(h) 305 if err != nil { 306 return nil, fmt.Errorf("failed to get Tx{%X}: %w", h, err) 307 } 308 hashString := string(h) 309 if _, ok := resultMap[hashString]; !ok { 310 resultMap[hashString] = struct{}{} 311 results = append(results, res) 312 } 313 // Potentially exit early. 314 select { 315 case <-ctx.Done(): 316 break RESULTS_LOOP 317 default: 318 } 319 } 320 321 return results, nil 322 } 323 324 func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) { 325 for _, c := range conditions { 326 if c.CompositeKey == types.TxHashKey { 327 decoded, err := hex.DecodeString(c.Operand.(string)) 328 return decoded, true, err 329 } 330 } 331 return 332 } 333 334 func (txi *TxIndex) setTmpHashes(tmpHeights map[string][]byte, it dbm.Iterator) { 335 eventSeq := extractEventSeqFromKey(it.Key()) 336 tmpHeights[string(it.Value())+eventSeq] = it.Value() 337 } 338 339 // match returns all matching txs by hash that meet a given condition and start 340 // key. An already filtered result (filteredHashes) is provided such that any 341 // non-intersecting matches are removed. 342 // 343 // NOTE: filteredHashes may be empty if no previous condition has matched. 344 func (txi *TxIndex) match( 345 ctx context.Context, 346 c query.Condition, 347 startKeyBz []byte, 348 filteredHashes map[string][]byte, 349 firstRun bool, 350 heightInfo HeightInfo, 351 ) map[string][]byte { 352 // A previous match was attempted but resulted in no matches, so we return 353 // no matches (assuming AND operand). 354 if !firstRun && len(filteredHashes) == 0 { 355 return filteredHashes 356 } 357 358 tmpHashes := make(map[string][]byte) 359 360 switch c.Op { 361 case query.OpEqual: 362 it, err := dbm.IteratePrefix(txi.store, startKeyBz) 363 if err != nil { 364 panic(err) 365 } 366 defer it.Close() 367 368 EQ_LOOP: 369 for ; it.Valid(); it.Next() { 370 371 // If we have a height range in a query, we need only transactions 372 // for this height 373 keyHeight, err := extractHeightFromKey(it.Key()) 374 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 375 continue 376 } 377 378 txi.setTmpHashes(tmpHashes, it) 379 // Potentially exit early. 380 select { 381 case <-ctx.Done(): 382 break EQ_LOOP 383 default: 384 } 385 } 386 if err := it.Error(); err != nil { 387 panic(err) 388 } 389 390 case query.OpExists: 391 // XXX: can't use startKeyBz here because c.Operand is nil 392 // (e.g. "account.owner/<nil>/" won't match w/ a single row) 393 it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey)) 394 if err != nil { 395 panic(err) 396 } 397 defer it.Close() 398 399 EXISTS_LOOP: 400 for ; it.Valid(); it.Next() { 401 keyHeight, err := extractHeightFromKey(it.Key()) 402 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 403 continue 404 } 405 txi.setTmpHashes(tmpHashes, it) 406 407 // Potentially exit early. 408 select { 409 case <-ctx.Done(): 410 break EXISTS_LOOP 411 default: 412 } 413 } 414 if err := it.Error(); err != nil { 415 panic(err) 416 } 417 418 case query.OpContains: 419 // XXX: startKey does not apply here. 420 // For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an" 421 // we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/" 422 it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey)) 423 if err != nil { 424 panic(err) 425 } 426 defer it.Close() 427 428 CONTAINS_LOOP: 429 for ; it.Valid(); it.Next() { 430 if !isTagKey(it.Key()) { 431 continue 432 } 433 434 if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { 435 keyHeight, err := extractHeightFromKey(it.Key()) 436 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 437 continue 438 } 439 txi.setTmpHashes(tmpHashes, it) 440 } 441 442 // Potentially exit early. 443 select { 444 case <-ctx.Done(): 445 break CONTAINS_LOOP 446 default: 447 } 448 } 449 if err := it.Error(); err != nil { 450 panic(err) 451 } 452 default: 453 panic("other operators should be handled already") 454 } 455 456 if len(tmpHashes) == 0 || firstRun { 457 // Either: 458 // 459 // 1. Regardless if a previous match was attempted, which may have had 460 // results, but no match was found for the current condition, then we 461 // return no matches (assuming AND operand). 462 // 463 // 2. A previous match was not attempted, so we return all results. 464 return tmpHashes 465 } 466 467 // Remove/reduce matches in filteredHashes that were not found in this 468 // match (tmpHashes). 469 REMOVE_LOOP: 470 for k, v := range filteredHashes { 471 tmpHash := tmpHashes[k] 472 if tmpHash == nil || !bytes.Equal(tmpHash, v) { 473 delete(filteredHashes, k) 474 475 // Potentially exit early. 476 select { 477 case <-ctx.Done(): 478 break REMOVE_LOOP 479 default: 480 } 481 } 482 } 483 484 return filteredHashes 485 } 486 487 // matchRange returns all matching txs by hash that meet a given queryRange and 488 // start key. An already filtered result (filteredHashes) is provided such that 489 // any non-intersecting matches are removed. 490 // 491 // NOTE: filteredHashes may be empty if no previous condition has matched. 492 func (txi *TxIndex) matchRange( 493 ctx context.Context, 494 qr indexer.QueryRange, 495 startKey []byte, 496 filteredHashes map[string][]byte, 497 firstRun bool, 498 heightInfo HeightInfo, 499 ) map[string][]byte { 500 // A previous match was attempted but resulted in no matches, so we return 501 // no matches (assuming AND operand). 502 if !firstRun && len(filteredHashes) == 0 { 503 return filteredHashes 504 } 505 506 tmpHashes := make(map[string][]byte) 507 508 it, err := dbm.IteratePrefix(txi.store, startKey) 509 if err != nil { 510 panic(err) 511 } 512 defer it.Close() 513 514 LOOP: 515 for ; it.Valid(); it.Next() { 516 if !isTagKey(it.Key()) { 517 continue 518 } 519 520 if _, ok := qr.AnyBound().(*big.Int); ok { 521 v := new(big.Int) 522 eventValue := extractValueFromKey(it.Key()) 523 v, ok := v.SetString(eventValue, 10) 524 if !ok { 525 continue LOOP 526 } 527 if qr.Key != types.TxHeightKey { 528 keyHeight, err := extractHeightFromKey(it.Key()) 529 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 530 continue LOOP 531 } 532 533 } 534 if checkBounds(qr, v) { 535 txi.setTmpHashes(tmpHashes, it) 536 } 537 538 // XXX: passing time in a ABCI Events is not yet implemented 539 // case time.Time: 540 // v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) 541 // if v == r.upperBound { 542 // break 543 // } 544 } 545 546 // Potentially exit early. 547 select { 548 case <-ctx.Done(): 549 break LOOP 550 default: 551 } 552 } 553 if err := it.Error(); err != nil { 554 panic(err) 555 } 556 557 if len(tmpHashes) == 0 || firstRun { 558 // Either: 559 // 560 // 1. Regardless if a previous match was attempted, which may have had 561 // results, but no match was found for the current condition, then we 562 // return no matches (assuming AND operand). 563 // 564 // 2. A previous match was not attempted, so we return all results. 565 return tmpHashes 566 } 567 568 // Remove/reduce matches in filteredHashes that were not found in this 569 // match (tmpHashes). 570 REMOVE_LOOP: 571 for k, v := range filteredHashes { 572 tmpHash := tmpHashes[k] 573 if tmpHash == nil || !bytes.Equal(tmpHashes[k], v) { 574 delete(filteredHashes, k) 575 576 // Potentially exit early. 577 select { 578 case <-ctx.Done(): 579 break REMOVE_LOOP 580 default: 581 } 582 } 583 } 584 585 return filteredHashes 586 } 587 588 // Keys 589 590 func isTagKey(key []byte) bool { 591 // Normally, if the event was indexed with an event sequence, the number of 592 // tags should 4. Alternatively it should be 3 if the event was not indexed 593 // with the corresponding event sequence. However, some attribute values in 594 // production can contain the tag separator. Therefore, the condition is >= 3. 595 numTags := strings.Count(string(key), tagKeySeparator) 596 return numTags >= 3 597 } 598 599 func extractHeightFromKey(key []byte) (int64, error) { 600 parts := strings.SplitN(string(key), tagKeySeparator, -1) 601 602 return strconv.ParseInt(parts[len(parts)-2], 10, 64) 603 } 604 func extractValueFromKey(key []byte) string { 605 keyString := string(key) 606 parts := strings.SplitN(keyString, tagKeySeparator, -1) 607 partsLen := len(parts) 608 value := strings.TrimPrefix(keyString, parts[0]+tagKeySeparator) 609 610 suffix := "" 611 suffixLen := 2 612 613 for i := 1; i <= suffixLen; i++ { 614 suffix = tagKeySeparator + parts[partsLen-i] + suffix 615 } 616 return strings.TrimSuffix(value, suffix) 617 618 } 619 620 func extractEventSeqFromKey(key []byte) string { 621 parts := strings.SplitN(string(key), tagKeySeparator, -1) 622 623 lastEl := parts[len(parts)-1] 624 625 if strings.Contains(lastEl, eventSeqSeparator) { 626 return strings.SplitN(lastEl, eventSeqSeparator, 2)[1] 627 } 628 return "0" 629 } 630 func keyForEvent(key string, value string, result *abci.TxResult, eventSeq int64) []byte { 631 return []byte(fmt.Sprintf("%s/%s/%d/%d%s", 632 key, 633 value, 634 result.Height, 635 result.Index, 636 eventSeqSeparator+strconv.FormatInt(eventSeq, 10), 637 )) 638 } 639 640 func keyForHeight(result *abci.TxResult) []byte { 641 return []byte(fmt.Sprintf("%s/%d/%d/%d%s", 642 types.TxHeightKey, 643 result.Height, 644 result.Height, 645 result.Index, 646 // Added to facilitate having the eventSeq in event keys 647 // Otherwise queries break expecting 5 entries 648 eventSeqSeparator+"0", 649 )) 650 } 651 652 func startKeyForCondition(c query.Condition, height int64) []byte { 653 if height > 0 { 654 return startKey(c.CompositeKey, c.Operand, height) 655 } 656 return startKey(c.CompositeKey, c.Operand) 657 } 658 659 func startKey(fields ...interface{}) []byte { 660 var b bytes.Buffer 661 for _, f := range fields { 662 b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator)) 663 } 664 return b.Bytes() 665 } 666 667 func checkBounds(ranges indexer.QueryRange, v *big.Int) bool { 668 include := true 669 lowerBound := ranges.LowerBoundValue() 670 upperBound := ranges.UpperBoundValue() 671 if lowerBound != nil && v.Cmp(lowerBound.(*big.Int)) == -1 { 672 include = false 673 } 674 675 if upperBound != nil && v.Cmp(upperBound.(*big.Int)) == 1 { 676 include = false 677 } 678 679 return include 680 } 681 682 //nolint:unused,deadcode 683 func lookForHeight(conditions []query.Condition) (height int64) { 684 for _, c := range conditions { 685 if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual { 686 return c.Operand.(int64) 687 } 688 } 689 return 0 690 }